Files
t5de-proxy/internal/products/chat.go
T
dhkatzandClaude Opus 5.5 887e8a2364 Serve community products from local packs
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]>
2026-10-29 03:17:14 -07:00

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
}