CI / build (push) Successful in 34s
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]>
84 lines
2.3 KiB
Go
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
|
|
}
|