T5 products in the client's inventory
CI / build (push) Successful in 25s

The inventory's product list (test.showInventory) gets the community products the person has: local packs, and
hub products in their library. Hiding one is answered by the proxy (and removes a hub product from the library).
When the library changes, the client is told to load its inventory again, with the server event IMVU sends
after a purchase. A product whose revision or info changes gets a new local ID, since the client keeps product
info for good; names lose characters the client's pages could take as HTML.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 18:59:52 -07:00
1 parent c281fb572c
commit ca7a86ce89
8 files changed
+225 -5

No files matched your search

+87 -2
View File
@@ -4,6 +4,9 @@ import (
"encoding/json"
"errors"
"net/http"
"net/url"
"path/filepath"
"sort"
"strconv"
"strings"
@@ -42,6 +45,8 @@ func (p *Products) Intercept(x *proxy.Exchange) error {
}
case "/api/base_look.php":
return p.interceptBaseLook(x)
case "/api/service/inventory/inventory_delete.php":
return p.interceptHide(x)
}
return nil
}
@@ -66,6 +71,10 @@ func (p *Products) interceptXMLRPC(x *proxy.Exchange) error {
return p.inventoryProductBatch(x, call)
case "test.authorizeProducts2":
return p.authorizeProducts2(x, call)
case "test.showInventory":
x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) {
return p.addToInventory(body)
}
}
return nil
}
@@ -241,8 +250,8 @@ func (s *served) productInfo(parent xmlrpc.Struct) xmlrpc.Struct {
own := map[string]xmlrpc.Value{
"products_id": s.local,
"manufacturers_id": int64(0),
"manufacturers_name": m.Creator,
"products_name": m.Name,
"manufacturers_name": plain(m.Creator),
"products_name": plain(m.Name),
"products_mature": mature,
"products_bundle": "",
"is_published": "1",
@@ -486,3 +495,79 @@ func (p *Products) Outgoing(queue, mount string, message []byte) ([]byte, bool)
p.logf("products: kept %d community products out of a %s sent to IMVU", len(r.products), command)
return m.withText(r.text), false
}
// plain keeps a name to characters the client's pages can show however they insert it: some insert creator and
// product names as HTML, in pages that can call into the client.
func plain(s string) string {
return strings.Map(func(r rune) rune {
switch r {
case '<', '>', '&', '"', '\'', '`':
return -1
}
return r
}, s)
}
// inventoryLocals is the community products in the inventory: local packs, and hub products in the library.
func (p *Products) inventoryLocals() []int64 {
p.refresh(false)
p.mu.Lock()
defer p.mu.Unlock()
var ids []int64
for id, s := range p.byID {
if p.HubDir != "" && filepath.Dir(s.dir) == filepath.Clean(p.HubDir) && (p.Owned == nil || !p.Owned(id)) {
continue
}
ids = append(ids, s.local)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids
}
// addToInventory adds the community products to test.showInventory's answer (a list of product IDs).
func (p *Products) addToInventory(body []byte) ([]byte, error) {
r, err := xmlrpc.ParseResponse(body)
if err != nil || r.Fault != nil || len(r.Params) != 1 {
return body, nil
}
list, ok := r.Params[0].([]xmlrpc.Value)
if !ok {
return body, nil
}
have := map[int64]bool{}
for _, v := range list {
if n, ok := v.(int64); ok {
have[n] = true
}
}
added := 0
for _, id := range p.inventoryLocals() {
if !have[id] {
list = append(list, id)
added++
}
}
if added == 0 {
return body, nil
}
r.Params[0] = list
p.logf("products: %d community products in the inventory", added)
return r.Marshal(), nil
}
// interceptHide answers hiding a community product in the inventory itself (IMVU doesn't know it).
func (p *Products) interceptHide(x *proxy.Exchange) error {
form, err := url.ParseQuery(string(x.Body))
if err != nil {
return nil
}
pid, err := strconv.ParseInt(form.Get("pid"), 10, 64)
if err != nil || !IsCommunity(pid) {
return nil
}
if id, ok := p.state.productID(pid); ok && p.OnHide != nil {
p.OnHide(id)
}
x.Response = localResponse(200, "application/json", []byte(`{"success": true}`))
return nil
}