Saving an outfit (POST /api/outfits.php) sent its community products to IMVU. They're now taken out (any ID in the community range, known or not), the request re-signed, and the products remembered under the outfit's ID, then added back when the client loads its saved outfits (marked AP if one is adult). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
400831bf3c
commit
6506c641f0
3 files changed
+157
No files matched your search
@@ -47,6 +47,8 @@ func (p *Products) Intercept(x *proxy.Exchange) error {
|
|||||||
return p.interceptBaseLook(x)
|
return p.interceptBaseLook(x)
|
||||||
case "/api/service/inventory/inventory_delete.php":
|
case "/api/service/inventory/inventory_delete.php":
|
||||||
return p.interceptHide(x)
|
return p.interceptHide(x)
|
||||||
|
case "/api/outfits.php":
|
||||||
|
return p.interceptOutfits(x)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -576,3 +578,102 @@ func (p *Products) interceptHide(x *proxy.Exchange) error {
|
|||||||
x.Response = localResponse(200, "application/json", []byte(`{"success": true}`))
|
x.Response = localResponse(200, "application/json", []byte(`{"success": true}`))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interceptOutfits keeps community products out of saved outfits IMVU stores (POST /api/outfits.php, JSON with
|
||||||
|
// "products": "1,2,3"), remembering them under the outfit's ID, and adds them back when the client loads saved
|
||||||
|
// outfits (GET /api/outfits.php?batch=... or ?outfit_id=...: {"result": [{"user_outfits_id", "pids", ...}]}).
|
||||||
|
func (p *Products) interceptOutfits(x *proxy.Exchange) error {
|
||||||
|
if len(x.Body) > 0 {
|
||||||
|
var save map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(x.Body, &save) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var products string
|
||||||
|
if json.Unmarshal(save["products"], &products) != nil {
|
||||||
|
return nil // not a save (e.g. set_default_outfit_id)
|
||||||
|
}
|
||||||
|
var kept []string
|
||||||
|
var community []string
|
||||||
|
stripped := 0
|
||||||
|
for _, f := range strings.Split(products, ",") {
|
||||||
|
id, err := strconv.ParseInt(strings.TrimSpace(f), 10, 64)
|
||||||
|
if err == nil && IsCommunity(id) {
|
||||||
|
stripped++ // always kept from IMVU, even if its product isn't known anymore
|
||||||
|
if pid, ok := p.state.productID(id); ok {
|
||||||
|
community = append(community, pid)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, f)
|
||||||
|
}
|
||||||
|
if stripped == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
save["products"], _ = json.Marshal(strings.Join(kept, ","))
|
||||||
|
body, _ := json.Marshal(save)
|
||||||
|
if !p.keys.resign(x.Request.Header, x.Body, x.URL.RawQuery, body, x.URL.RawQuery) {
|
||||||
|
return errNoKey
|
||||||
|
}
|
||||||
|
x.Body = body
|
||||||
|
p.logf("products: kept %d community products out of an outfit saved to IMVU", stripped)
|
||||||
|
x.ModifyResponse = func(resp *http.Response, b []byte) ([]byte, error) {
|
||||||
|
var r struct {
|
||||||
|
Result struct {
|
||||||
|
OutfitID json.Number `json:"outfit_id"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(b, &r) == nil && r.Result.OutfitID != "" {
|
||||||
|
if err := p.state.setOutfit(r.Result.OutfitID.String(), community); err != nil {
|
||||||
|
p.logf("products: remembering an outfit: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
x.ModifyResponse = func(resp *http.Response, b []byte) ([]byte, error) {
|
||||||
|
var r map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(b, &r) != nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
var outfits []map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(r["result"], &outfits) != nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for _, o := range outfits {
|
||||||
|
var id json.Number
|
||||||
|
if json.Unmarshal(o["user_outfits_id"], &id) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
products := p.state.outfit(id.String())
|
||||||
|
if len(products) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var pids []json.RawMessage
|
||||||
|
json.Unmarshal(o["pids"], &pids)
|
||||||
|
adult := false
|
||||||
|
for _, pid := range products {
|
||||||
|
if s := p.byProductID(pid); s != nil {
|
||||||
|
pids = append(pids, json.RawMessage(strconv.FormatInt(s.local, 10)))
|
||||||
|
adult = adult || s.pack.Manifest.Rating == "adult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o["pids"], _ = json.Marshal(pids)
|
||||||
|
if adult {
|
||||||
|
var restricted string
|
||||||
|
json.Unmarshal(o["restricted"], &restricted)
|
||||||
|
if restricted == "" || restricted == "0" {
|
||||||
|
o["restricted"], _ = json.Marshal("1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
r["result"], _ = json.Marshal(outfits)
|
||||||
|
return json.Marshal(r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -73,6 +73,13 @@ func (f *fakeIMVU) RoundTrip(r *http.Request) (*http.Response, error) {
|
|||||||
return reply(`{"status": "success"}`)
|
return reply(`{"status": "success"}`)
|
||||||
}
|
}
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
|
case "/api/outfits.php":
|
||||||
|
if len(body) > 0 {
|
||||||
|
checkSigned(false)
|
||||||
|
f.record("save outfit", body)
|
||||||
|
return reply(`{"result": {"outfit_id": 555}}`)
|
||||||
|
}
|
||||||
|
return reply(`{"result": [{"user_outfits_id": "555", "pids": [80, 1], "restricted": "0"}, {"user_outfits_id": "9", "pids": [80]}]}`)
|
||||||
case "/api/xmlrpc/chat.php":
|
case "/api/xmlrpc/chat.php":
|
||||||
f.record("chat", body)
|
f.record("chat", body)
|
||||||
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{
|
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{
|
||||||
@@ -361,3 +368,33 @@ func TestInventory(t *testing.T) {
|
|||||||
t.Fatalf("product of the new local ID: %q", id)
|
t.Fatalf("product of the new local ID: %q", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSavedOutfits(t *testing.T) {
|
||||||
|
h, _ := newHarness(t)
|
||||||
|
local := h.p.byProductID("blue-coat").local
|
||||||
|
h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false)
|
||||||
|
|
||||||
|
// Saving: IMVU gets the outfit without the community product.
|
||||||
|
// (2100000000 is a community ID the proxy doesn't know: it's kept from IMVU all the same.)
|
||||||
|
save := []byte(fmt.Sprintf(`{"outfit_name": "Mine", "products": "80,1,%d,2100000000", "privacy": 1}`, local))
|
||||||
|
if status, _ := h.do("POST", "https://client-dynamic.imvu.com/api/outfits.php", save, true, false); status != 200 {
|
||||||
|
t.Fatalf("status %d", status)
|
||||||
|
}
|
||||||
|
if got := h.imvu.asked["save outfit"]; len(got) != 1 || !strings.Contains(got[0], `"products":"80,1"`) {
|
||||||
|
t.Fatalf("IMVU got %q", got)
|
||||||
|
}
|
||||||
|
// Loading saved outfits: it's back in that outfit only.
|
||||||
|
_, body := h.do("GET", "https://client-dynamic.imvu.com/api/outfits.php?batch=555,9", nil, false, false)
|
||||||
|
var r struct {
|
||||||
|
Result []struct {
|
||||||
|
ID string `json:"user_outfits_id"`
|
||||||
|
Pids []int64 `json:"pids"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &r); err != nil {
|
||||||
|
t.Fatal(err, string(body))
|
||||||
|
}
|
||||||
|
if fmt.Sprint(r.Result[0].Pids) != fmt.Sprint([]int64{80, 1, local}) || fmt.Sprint(r.Result[1].Pids) != "[80]" {
|
||||||
|
t.Fatalf("outfits %s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ type state struct {
|
|||||||
type stateData struct {
|
type stateData struct {
|
||||||
IDs map[string]int64 `json:"ids"` // product ID -> local ID
|
IDs map[string]int64 `json:"ids"` // product ID -> local ID
|
||||||
Worn map[string]wornOutfit `json:"worn"` // IMVU user ID -> what they wear
|
Worn map[string]wornOutfit `json:"worn"` // IMVU user ID -> what they wear
|
||||||
|
Outfits map[string][]string `json:"outfits,omitempty"` // saved outfit ID -> its community products
|
||||||
}
|
}
|
||||||
|
|
||||||
// wornOutfit is an account's outfit as last saved or sent: the IMVU products IMVU got, and the community products
|
// wornOutfit is an account's outfit as last saved or sent: the IMVU products IMVU got, and the community products
|
||||||
@@ -109,6 +110,24 @@ func (s *state) setWorn(user string, imvu []int64, products []string) error {
|
|||||||
return s.saveLocked()
|
return s.saveLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outfit is the community products saved with an outfit (by IMVU's outfit ID).
|
||||||
|
func (s *state) outfit(id string) []string {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return append([]string(nil), s.data.Outfits[id]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOutfit remembers the community products saved with an outfit.
|
||||||
|
func (s *state) setOutfit(id string, products []string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.data.Outfits == nil {
|
||||||
|
s.data.Outfits = map[string][]string{}
|
||||||
|
}
|
||||||
|
s.data.Outfits[id] = append([]string(nil), products...)
|
||||||
|
return s.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
// sameSet reports whether a and b hold the same IDs, in any order.
|
// sameSet reports whether a and b hold the same IDs, in any order.
|
||||||
func sameSet(a, b []int64) bool {
|
func sameSet(a, b []int64) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
|
|||||||
Reference in new issue
Block a user