// Package cards fixes what IMVU's servers no longer send the client's avatar cards: the AP+ badge. The card shows it // when avatarcard.php's answer has show_ap_plus, which IMVU stopped sending; whether someone has AP+ is public on // their IMVU profile (api.imvu.com/user/user-, is_ap_plus), as the friends list and Find People still show. package cards import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "time" "git.t5de.dev/t5de/t5de-proxy/internal/proxy" ) // keep is how long an account's AP+ is remembered. const keep = 10 * time.Minute // Cards adds show_ap_plus to avatar cards of people with AP+. type Cards struct { // Client fetches IMVU's public profiles (nil: http.DefaultClient). Client *http.Client // Base is IMVU's API ("" for https://api.imvu.com). Base string mu sync.Mutex known map[int64]apPlus } type apPlus struct { has bool at time.Time } func (c *Cards) Intercept(x *proxy.Exchange) error { if !strings.EqualFold(x.URL.Hostname(), "client-dynamic.imvu.com") || x.URL.Path != "/api/avatarcard.php" { return nil } cid, err := strconv.ParseInt(x.URL.Query().Get("cid"), 10, 64) if err != nil || cid <= 0 { return nil } x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) { var card map[string]json.RawMessage if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &card) != nil || set(card["show_ap_plus"]) { return body, nil } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if !c.has(ctx, cid) { return body, nil } card["show_ap_plus"] = json.RawMessage("1") return json.Marshal(card) } return nil } // set reports whether a JSON value counts as set in the card's JavaScript (truthy). func set(v json.RawMessage) bool { s := strings.TrimSpace(string(v)) return s != "" && s != "null" && s != "false" && s != "0" && s != `""` && s != `"0"` } // has reports whether an account has AP+ (false if IMVU can't say). func (c *Cards) has(ctx context.Context, id int64) bool { c.mu.Lock() k, ok := c.known[id] c.mu.Unlock() if ok && time.Since(k.at) < keep { return k.has } base := c.Base if base == "" { base = "https://api.imvu.com" } u := fmt.Sprintf("%s/user/user-%d", base, id) req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { return false } client := c.Client if client == nil { client = http.DefaultClient } resp, err := client.Do(req) if err != nil { return k.has } defer resp.Body.Close() var profile struct { Denormalized map[string]struct { Data struct { IsAPPlus bool `json:"is_ap_plus"` } `json:"data"` } `json:"denormalized"` } if resp.StatusCode != http.StatusOK || json.NewDecoder(resp.Body).Decode(&profile) != nil { return k.has } has := profile.Denormalized[fmt.Sprintf("https://api.imvu.com/user/user-%d", id)].Data.IsAPPlus c.mu.Lock() if c.known == nil { c.known = map[int64]apPlus{} } c.known[id] = apPlus{has: has, at: time.Now()} c.mu.Unlock() return has }