The proxy serves products from local packs (t5de-api's format) under local IDs from 2,000,000,000: product info and permission answers (with the parent's derivation chain from IMVU), the product's CFL, and the shop's bundle lookup. Community products are taken out of outfit saves and outfit chat commands before IMVU sees them, remembered per account with the outfit they belong to, and added back when the client loads that outfit. Changed requests are re-signed with the session key from the login response. Co-Authored-By: Claude Opus 5.5 <[email protected]>
85 lines
2.4 KiB
Go
85 lines
2.4 KiB
Go
package products
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// outfitCommands are the chat commands whose arguments are product IDs: putting products on and taking them off.
|
|
var outfitCommands = map[string]bool{
|
|
"*use": true, "*putOn": true, "*putOnOutfit": true, "*remove": true, "*takeOff": true,
|
|
}
|
|
|
|
// bodyProduct is the avatar body every full outfit starts from; a *use list with it is a whole outfit (the client
|
|
// re-sends its outfit that way every 30 s or so).
|
|
const bodyProduct = 80
|
|
|
|
// strippedCommand is a chat line with community products taken out of its outfit command.
|
|
type strippedCommand struct {
|
|
text string // the line to send instead
|
|
changed bool // products were taken out
|
|
empty bool // nothing is left to send
|
|
outfit bool // the line was a whole outfit (*use with the body)
|
|
products []int64 // the community products it named
|
|
kept []int64 // the other products it named
|
|
}
|
|
|
|
// stripCommand takes products for which isCommunity is true out of an outfit command's arguments.
|
|
func stripCommand(text string, isCommunity func(int64) bool) strippedCommand {
|
|
fields := strings.Fields(text)
|
|
if len(fields) == 0 || !outfitCommands[fields[0]] {
|
|
return strippedCommand{text: text}
|
|
}
|
|
r := strippedCommand{text: text}
|
|
kept := []string{fields[0]}
|
|
for _, f := range fields[1:] {
|
|
id, err := strconv.ParseInt(f, 10, 64)
|
|
if err == nil && id == bodyProduct && fields[0] == "*use" {
|
|
r.outfit = true
|
|
}
|
|
if err == nil && isCommunity(id) {
|
|
r.products = append(r.products, id)
|
|
continue
|
|
}
|
|
if err == nil {
|
|
r.kept = append(r.kept, id)
|
|
}
|
|
kept = append(kept, f)
|
|
}
|
|
if len(r.products) == 0 {
|
|
return r
|
|
}
|
|
r.changed = true
|
|
r.empty = len(kept) == 1
|
|
r.text = strings.Join(kept, " ")
|
|
return r
|
|
}
|
|
|
|
// chatMessage is the JSON the client sends on a chat queue's "messages" mount.
|
|
type chatMessage struct {
|
|
fields map[string]json.RawMessage
|
|
text string
|
|
userID int64
|
|
}
|
|
|
|
func parseChatMessage(b []byte) (*chatMessage, bool) {
|
|
var fields map[string]json.RawMessage
|
|
if json.Unmarshal(b, &fields) != nil {
|
|
return nil, false
|
|
}
|
|
m := &chatMessage{fields: fields}
|
|
if json.Unmarshal(fields["message"], &m.text) != nil {
|
|
return nil, false
|
|
}
|
|
json.Unmarshal(fields["userId"], &m.userID)
|
|
return m, true
|
|
}
|
|
|
|
func (m *chatMessage) withText(text string) []byte {
|
|
b, _ := json.Marshal(text)
|
|
m.fields["message"] = b
|
|
out, _ := json.Marshal(m.fields)
|
|
return out
|
|
}
|