diff --git a/internal/products/intercept.go b/internal/products/intercept.go index aceeb42..5a663d8 100644 --- a/internal/products/intercept.go +++ b/internal/products/intercept.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "path/filepath" + "regexp" "sort" "strconv" "strings" @@ -33,6 +34,14 @@ func (p *Products) Intercept(x *proxy.Exchange) error { if host == "api.imvu.com" && participantPath.MatchString(path) { return p.interceptParticipant(x) } + if host == "api.imvu.com" { + if m := productPath.FindStringSubmatch(path); m != nil { + if id, ok := parseID(m[1]); ok && IsCommunity(id) { + x.Response = productAPIAnswer(path, m[2]) + return nil + } + } + } switch path { case "/api/xmlrpc/chat.php": return p.interceptChat(x) @@ -59,6 +68,21 @@ func (p *Products) Intercept(x *proxy.Exchange) error { return nil } +// productPath is IMVU's REST API for a product (api.imvu.com/product/product-[/]). +var productPath = regexp.MustCompile(`^/product/product-([0-9.]+)(/[a-z_]+)?$`) + +// productAPIAnswer answers the REST API about a community product: no related products (uml_products, which the +// shop's recommender asks for when a product is tried on), and nothing else. +func productAPIAnswer(path, relation string) *http.Response { + id := "https://api.imvu.com" + path + if relation == "/uml_products" { + b, _ := json.Marshal(map[string]any{"status": "success", "id": id, "denormalized": map[string]any{ + id: map[string]any{"data": map[string]any{"total_count": 0, "items": []any{}}, "relations": map[string]any{}}}}) + return localResponse(200, "application/json", b) + } + return localResponse(404, "application/json", []byte(`{"status":"failure","error":"NOT_FOUND"}`)) +} + func (p *Products) interceptXMLRPC(x *proxy.Exchange) error { call, err := xmlrpc.ParseCall(x.Body) if err != nil { diff --git a/internal/products/shop_test.go b/internal/products/shop_test.go index dd46a7a..fc80965 100644 --- a/internal/products/shop_test.go +++ b/internal/products/shop_test.go @@ -112,6 +112,12 @@ func TestShop(t *testing.T) { } h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false) + // Trying one on asks IMVU's REST API for related products: answered here, with none. + status, b := h.do("GET", fmt.Sprintf("https://api.imvu.com/product/product-%d/uml_products?limit=6", coatLocal), nil, false, false) + if status != 200 || !strings.Contains(string(b), `"total_count":0`) { + t.Fatalf("uml_products: %d %s", status, b) + } + // Buying a T5 product adds it to the library; IMVU hears only of its own products. // Product details: community ones answered here, IMVU asked only for its own. info := func(pids ...any) []map[string]any {