package products import ( "encoding/json" "errors" "io/fs" "os" "path/filepath" "sort" "sync" ) // FirstLocalID is where the client's numbers for community products start. IMVU's product IDs are far below // (about 72 million in 2026), and the client's XML-RPC can't carry numbers past 2^31-1. const FirstLocalID = 2_000_000_000 // state is what the proxy remembers between runs, in /products.json: the local ID of every product it // has served (so outfits keep naming the same product), and the community products each IMVU account wears (IMVU // never sees them, so it can't remember them). type state struct { mu sync.Mutex path string data stateData } type stateData struct { IDs map[string]int64 `json:"ids"` // product ID -> local ID Worn map[string][]string `json:"worn"` // IMVU user ID -> product IDs } func loadState(path string) (*state, error) { s := &state{path: path, data: stateData{IDs: map[string]int64{}, Worn: map[string][]string{}}} b, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return s, nil } if err != nil { return nil, err } if err := json.Unmarshal(b, &s.data); err != nil { return nil, err } if s.data.IDs == nil { s.data.IDs = map[string]int64{} } if s.data.Worn == nil { s.data.Worn = map[string][]string{} } return s, nil } // localID is the product's local ID, given one if it has none yet. func (s *state) localID(product string) (int64, error) { s.mu.Lock() defer s.mu.Unlock() if id, ok := s.data.IDs[product]; ok { return id, nil } next := int64(FirstLocalID) for _, id := range s.data.IDs { next = max(next, id+1) } if next > 2_147_483_647 { return 0, errors.New("products: out of local IDs") } s.data.IDs[product] = next return next, s.saveLocked() } // worn is the community products the user wears. func (s *state) worn(user string) []string { s.mu.Lock() defer s.mu.Unlock() return append([]string(nil), s.data.Worn[user]...) } // setWorn records the community products the user wears. func (s *state) setWorn(user string, products []string) error { products = append([]string(nil), products...) sort.Strings(products) s.mu.Lock() defer s.mu.Unlock() if equal(s.data.Worn[user], products) { return nil } if len(products) == 0 { delete(s.data.Worn, user) } else { s.data.Worn[user] = products } return s.saveLocked() } func equal(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func (s *state) saveLocked() error { b, err := json.MarshalIndent(s.data, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o600); err != nil { return err } return os.Rename(tmp, s.path) }