- The inventory's Recently Added puts T5 products added to the library in the last 30 days first. - The shop's T5 listing follows its sort menu: Best Match ranks by name, Newest by upload date (oldest first ascending); ratings and prices (T5 products have neither) sort by newest. - Outfit cards: outfit_products.php gets the outfit's community products added (not for sale), find_bundle.php is asked about IMVU's products only, and removing one in the card (remove_pids) keeps it from IMVU and forgets it in the outfit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
af9158513b
commit
171e8173af
11 files changed
+463
-7
No files matched your search
@@ -0,0 +1,175 @@
|
||||
package products
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
|
||||
)
|
||||
|
||||
// The client's outfit card (dialogs/outfit_card) lists an outfit's products from api/outfit_products.php, asks
|
||||
// api/find_bundle.php whether the outfit's products are a bundle on sale, and saves changes (removing products with
|
||||
// remove_pids) to api/outfits.php. IMVU only has the outfit without its community products (see interceptOutfits),
|
||||
// so the proxy adds them to the list, and keeps them out of the rest.
|
||||
|
||||
// interceptOutfitProducts adds an outfit's community products to its product list.
|
||||
func (p *Products) interceptOutfitProducts(x *proxy.Exchange) error {
|
||||
outfit := x.URL.Query().Get("outfit_id")
|
||||
products := p.state.outfit(outfit)
|
||||
if outfit == "" || len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) {
|
||||
var r map[string]json.RawMessage
|
||||
if json.Unmarshal(body, &r) != nil {
|
||||
return body, nil
|
||||
}
|
||||
var list map[string]json.RawMessage
|
||||
if json.Unmarshal(r["result"], &list) != nil {
|
||||
if string(r["result"]) != "[]" { // PHP's empty object
|
||||
p.logf("products: outfit_products.php: unexpected result")
|
||||
return body, nil
|
||||
}
|
||||
list = map[string]json.RawMessage{}
|
||||
}
|
||||
for _, id := range products {
|
||||
if row, ok := p.outfitCardRow(id); ok {
|
||||
list[strconv.FormatInt(row.local, 10)], _ = json.Marshal(row.entry())
|
||||
}
|
||||
}
|
||||
r["result"], _ = json.Marshal(list)
|
||||
return json.Marshal(r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cardRow struct {
|
||||
hub.Product
|
||||
local int64
|
||||
}
|
||||
|
||||
// entry is the outfit card's product entry. Community products can't be in a bundle for sale on IMVU.
|
||||
func (r cardRow) entry() map[string]any {
|
||||
category, categoryID := "", int64(0)
|
||||
if n := len(r.Categories); n > 0 {
|
||||
category, categoryID = categoryText(r.Categories[n-1].Name), r.Categories[n-1].ID
|
||||
}
|
||||
ap := 0
|
||||
if r.Rating == "adult" {
|
||||
ap = 1
|
||||
}
|
||||
return map[string]any{"name": plain(r.Name), "image": r.Image, "category_id": categoryID, "category": category,
|
||||
"is_ap": ap, "is_ap_plus": 0, "is_ufi": 0, "is_bundleable": 0}
|
||||
}
|
||||
|
||||
// outfitCardRow is a community product as the outfit card shows it: from the library or the shop's listing if the
|
||||
// hub listed it, else from its pack.
|
||||
func (p *Products) outfitCardRow(id string) (cardRow, bool) {
|
||||
s := p.byProductID(id)
|
||||
if s == nil {
|
||||
return cardRow{}, false
|
||||
}
|
||||
if p.Library != nil {
|
||||
for _, e := range p.Library() {
|
||||
if e.Product.ID == id {
|
||||
return cardRow{e.Product, s.local}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
m := s.pack.Manifest
|
||||
p.mu.Lock()
|
||||
hp, ok := p.shopSeen[id]
|
||||
p.mu.Unlock()
|
||||
if !ok {
|
||||
hp = hub.Product{ID: id, Name: m.Name, Creator: m.Creator, Rating: m.Rating}
|
||||
}
|
||||
return cardRow{hp, s.local}, true
|
||||
}
|
||||
|
||||
// interceptFindBundle asks IMVU about the outfit's IMVU products only (find_bundle.php?pids=1,2,...).
|
||||
func (p *Products) interceptFindBundle(x *proxy.Exchange) error {
|
||||
parts := strings.Split(x.URL.RawQuery, "&")
|
||||
changed := false
|
||||
for i, part := range parts {
|
||||
name, value, ok := strings.Cut(part, "=")
|
||||
if !ok || name != "pids" {
|
||||
continue
|
||||
}
|
||||
var kept []string
|
||||
for _, f := range strings.Split(value, ",") {
|
||||
if id, ok := parseID(f); ok && IsCommunity(id) {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, f)
|
||||
}
|
||||
parts[i] = name + "=" + strings.Join(kept, ",")
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
query := strings.Join(parts, "&")
|
||||
if !p.keys.resign(x.Request.Header, x.Body, x.URL.RawQuery, x.Body, query) {
|
||||
return errNoKey
|
||||
}
|
||||
x.URL.RawQuery = query
|
||||
x.Request.URL.RawQuery = query
|
||||
return nil
|
||||
}
|
||||
|
||||
// outfitRemovals takes community products out of an outfit card save's remove_pids, and out of the outfit as
|
||||
// remembered here. It reports whether the body changed.
|
||||
func (p *Products) outfitRemovals(save map[string]json.RawMessage) bool {
|
||||
var remove []json.RawMessage
|
||||
if json.Unmarshal(save["remove_pids"], &remove) != nil {
|
||||
return false
|
||||
}
|
||||
var kept []json.RawMessage
|
||||
var gone []string
|
||||
for _, raw := range remove {
|
||||
if id, ok := rawID(raw); ok && IsCommunity(id) {
|
||||
if pid, ok := p.state.productID(id); ok {
|
||||
gone = append(gone, pid)
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, raw)
|
||||
}
|
||||
if len(kept) == len(remove) {
|
||||
return false
|
||||
}
|
||||
if kept == nil {
|
||||
kept = []json.RawMessage{}
|
||||
}
|
||||
save["remove_pids"], _ = json.Marshal(kept)
|
||||
var outfit json.Number
|
||||
if json.Unmarshal(save["outfit_id"], &outfit) != nil {
|
||||
var s string
|
||||
json.Unmarshal(save["outfit_id"], &s)
|
||||
outfit = json.Number(s)
|
||||
}
|
||||
if outfit != "" && len(gone) > 0 {
|
||||
var left []string
|
||||
for _, pid := range p.state.outfit(outfit.String()) {
|
||||
if !contains(gone, pid) {
|
||||
left = append(left, pid)
|
||||
}
|
||||
}
|
||||
if err := p.state.setOutfit(outfit.String(), left); err != nil {
|
||||
p.logf("products: remembering an outfit: %v", err)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user