Files
t5de-proxy/internal/products/chat.go
T
dhkatzandClaude Opus 5.5 2280ad6314
CI / build (push) Successful in 34s
Show T5 products in the client's shop; keep float IDs and unknown paths off IMVU
The shop's T5 filter (maturity_rating "t5") is answered from the hub's products, fetched so they can be
shown and tried on; buying T5 products adds them to the hub library, and product.php details for
community products are answered here.

The client sends some product IDs as floats ("2000000008.0"): product.php passed the worn community
product to IMVU that way. Every ID is now read either way, and products.Guard refuses any request to
IMVU that still names a local ID given out. T5DE_PROXY_DUMP logs request and response bodies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:15:16 -07:00

84 lines
2.3 KiB
Go

package products
import (
"encoding/json"
"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, ok := parseID(f)
if ok && id == bodyProduct && fields[0] == "*use" {
r.outfit = true
}
if ok && isCommunity(id) {
r.products = append(r.products, id)
continue
}
if ok {
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
}