Products: chat command filter, request signing, saved state
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2190230f81
commit
0c4f6caec1
4 files changed
+313
No files matched your search
@@ -0,0 +1,119 @@
|
||||
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 <data dir>/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)
|
||||
}
|
||||
Reference in new issue
Block a user