CI / build (push) Successful in 34s
- 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]>
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
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
|
|
}
|