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 }