Community products are taken out of rooms their owners save and broadcast, kept on the hub per room, and added back when anyone with T5DE loads the room or gets its owner's broadcast; the hub's decor notice resends the room with the new ones. Placements are stored with the hub's product ID. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d8332b21a8
commit
0be2cd8f4e
9 files changed
+685
-2
No files matched your search
@@ -0,0 +1,527 @@
|
||||
package products
|
||||
|
||||
// Decorations: community furniture (and poses, which are furniture) placed in IMVU rooms.
|
||||
//
|
||||
// IMVU keeps the room without them: the proxy takes community products out of the room its owner saves
|
||||
// (test.setRoomState) and broadcasts ("*imvu:setRoomState <scene>" in the room's chat), and stores them on the hub
|
||||
// for that room (hub products only; a local pack's can't be shared). When anyone with T5DE loads the room
|
||||
// (test.getRoomState) or gets its owner's broadcast, the proxy adds the room's decorations back, as local IDs. The
|
||||
// hub tells everyone in the room when its owner changes them ("decor"), and the proxy gives the client the room
|
||||
// again with the new ones.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/imq"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/xmlrpc"
|
||||
)
|
||||
|
||||
// DecorItem is a decoration: a furniture instance ID, a hub product ID, and the placement, as IMVU's
|
||||
// room_contents has them (as a JSON array).
|
||||
type DecorItem struct {
|
||||
Furni int64
|
||||
Product string
|
||||
Placement string
|
||||
}
|
||||
|
||||
func (d DecorItem) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([]any{d.Furni, d.Product, d.Placement})
|
||||
}
|
||||
|
||||
func (d *DecorItem) UnmarshalJSON(b []byte) error {
|
||||
var a []json.RawMessage
|
||||
if err := json.Unmarshal(b, &a); err != nil || len(a) != 3 {
|
||||
return err
|
||||
}
|
||||
json.Unmarshal(a[0], &d.Furni)
|
||||
json.Unmarshal(a[1], &d.Product)
|
||||
return json.Unmarshal(a[2], &d.Placement)
|
||||
}
|
||||
|
||||
// decorations is the proxy's view of rooms' decorations, under its own lock.
|
||||
type decorations struct {
|
||||
mu sync.Mutex
|
||||
rooms map[string]decorEntry // room instance -> decorations
|
||||
base map[string][]any // room instance -> its contents as IMVU last had them (for giving the room again)
|
||||
info map[string]any // room instance -> its room_info, as last seen
|
||||
}
|
||||
|
||||
type decorEntry struct {
|
||||
items []DecorItem
|
||||
at time.Time
|
||||
}
|
||||
|
||||
// decorFor is how long decorations are used before being asked for again (the hub also says when they change).
|
||||
const decorFor = 5 * time.Minute
|
||||
|
||||
func (d *decorations) get(instance string) ([]DecorItem, bool) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
e, ok := d.rooms[instance]
|
||||
if !ok || time.Since(e.at) > decorFor {
|
||||
return nil, false
|
||||
}
|
||||
return e.items, true
|
||||
}
|
||||
|
||||
func (d *decorations) set(instance string, items []DecorItem) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.rooms == nil {
|
||||
d.rooms = map[string]decorEntry{}
|
||||
}
|
||||
d.rooms[instance] = decorEntry{items, time.Now()}
|
||||
}
|
||||
|
||||
func (d *decorations) setBase(instance string, contents []any, info any) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.base == nil {
|
||||
d.base, d.info = map[string][]any{}, map[string]any{}
|
||||
}
|
||||
d.base[instance] = contents
|
||||
if info != nil {
|
||||
d.info[instance] = info
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decorations) getBase(instance string) ([]any, any) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.base[instance], d.info[instance]
|
||||
}
|
||||
|
||||
// infoMap is a room_info as JSON for a scene (XML-RPC structs become objects).
|
||||
func infoMap(v any) any {
|
||||
s, ok := v.(xmlrpc.Struct)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
m := map[string]any{}
|
||||
for _, mem := range s {
|
||||
m[mem.Name] = mem.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// roomOwner is the owner of an IMVU room instance ("<owner>-<n>").
|
||||
func roomOwner(instance string) int64 {
|
||||
owner, _, ok := strings.Cut(instance, "-")
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
id, _ := strconv.ParseInt(owner, 10, 64)
|
||||
return id
|
||||
}
|
||||
|
||||
// isHostedInstance reports whether a room is hosted by the hub (its scene is the hosted package's).
|
||||
func isHostedInstance(instance string) bool {
|
||||
return strings.HasPrefix(instance, strconv.Itoa(HostedOwnerID)+"-")
|
||||
}
|
||||
|
||||
// decorFetch gets a room's decorations, from memory or the hub.
|
||||
func (p *Products) decorFetch(ctx context.Context, instance string) []DecorItem {
|
||||
if items, ok := p.decor.get(instance); ok {
|
||||
return items
|
||||
}
|
||||
if p.Decorations == nil {
|
||||
return nil
|
||||
}
|
||||
items, err := p.Decorations(ctx, instance)
|
||||
if err != nil {
|
||||
p.logf("decor: %s: %v", instance, err)
|
||||
return nil
|
||||
}
|
||||
p.decor.set(instance, items)
|
||||
return items
|
||||
}
|
||||
|
||||
// decorLocals is decorations as room_contents rows with local IDs (asking for products that aren't here yet).
|
||||
func (p *Products) decorLocals(items []DecorItem) []any {
|
||||
var rows []any
|
||||
for _, it := range items {
|
||||
local, ok := p.LocalID(it.Product)
|
||||
if !ok {
|
||||
if p.Want != nil {
|
||||
p.Want(it.Product)
|
||||
}
|
||||
continue
|
||||
}
|
||||
rows = append(rows, []any{it.Furni, local, swapToken(it.Placement, it.Product, strconv.FormatInt(local, 10))})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// swapToken replaces a whole space-separated token of a placement: the placement names its own product, which is
|
||||
// stored as the hub's product ID and given to the client as its local ID.
|
||||
func swapToken(placement, from, to string) string {
|
||||
fields := strings.Split(placement, " ")
|
||||
for i, f := range fields {
|
||||
if f == from {
|
||||
fields[i] = to
|
||||
}
|
||||
}
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
// splitCommunity takes community products out of room_contents rows: the rest, and the community ones as
|
||||
// decorations (hub products; others are dropped).
|
||||
func (p *Products) splitCommunity(rows []any) (rest []any, items []DecorItem, stripped int) {
|
||||
for _, row := range rows {
|
||||
r, ok := row.([]any)
|
||||
if !ok || len(r) < 3 {
|
||||
rest = append(rest, row)
|
||||
continue
|
||||
}
|
||||
product, ok := numberOf(r[1])
|
||||
if !ok || !IsCommunity(product) {
|
||||
rest = append(rest, row)
|
||||
continue
|
||||
}
|
||||
stripped++
|
||||
id, ok := p.state.productID(product)
|
||||
if !ok || !p.inHubDir(id) {
|
||||
p.logf("decor: a room has %d, which isn't a hub product; it can't be shared", product)
|
||||
continue
|
||||
}
|
||||
furni, _ := numberOf(r[0])
|
||||
placement, _ := r[2].(string)
|
||||
items = append(items, DecorItem{furni, id, swapToken(placement, strconv.FormatInt(product, 10), id)})
|
||||
}
|
||||
return rest, items, stripped
|
||||
}
|
||||
|
||||
func numberOf(v any) (int64, bool) {
|
||||
switch v := v.(type) {
|
||||
case int64:
|
||||
return v, true
|
||||
case float64:
|
||||
return int64(v), v == float64(int64(v))
|
||||
case json.Number:
|
||||
n, err := v.Int64()
|
||||
return n, err == nil
|
||||
case string:
|
||||
return parseID(v)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// saveDecor stores a room's decorations on the hub, if the client is its owner and they changed.
|
||||
func (p *Products) saveDecor(instance string, items []DecorItem) {
|
||||
if roomOwner(instance) != p.User() || p.SaveDecorations == nil {
|
||||
return
|
||||
}
|
||||
old, known := p.decor.get(instance)
|
||||
if known && sameDecor(old, items) {
|
||||
return
|
||||
}
|
||||
p.decor.set(instance, items)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := p.SaveDecorations(ctx, instance, items); err != nil {
|
||||
p.logf("decor: saving %s: %v", instance, err)
|
||||
return
|
||||
}
|
||||
p.logf("decor: %d community products in %s, kept on T5DE's hub", len(items), instance)
|
||||
}()
|
||||
}
|
||||
|
||||
func sameDecor(a, b []DecorItem) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// --- XML-RPC: saving and loading rooms ---
|
||||
|
||||
func (p *Products) interceptSetRoomState(x *proxy.Exchange, call *xmlrpc.Call) error {
|
||||
args, ok := call.Params[0].(xmlrpc.Struct)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
instance := member(args, "room_instance_id")
|
||||
if instance == "" || isHostedInstance(instance) {
|
||||
return nil
|
||||
}
|
||||
v, _ := args.Get("room_contents")
|
||||
rows, ok := v.([]xmlrpc.Value)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rest, items, stripped := p.splitCommunity(rows)
|
||||
_, hadDecor := p.decor.get(instance)
|
||||
if stripped > 0 || hadDecor {
|
||||
p.saveDecor(instance, items)
|
||||
}
|
||||
if stripped == 0 {
|
||||
return nil
|
||||
}
|
||||
if rest == nil {
|
||||
rest = []xmlrpc.Value{}
|
||||
}
|
||||
changed := make(xmlrpc.Struct, len(args))
|
||||
copy(changed, args)
|
||||
for i := range changed {
|
||||
if changed[i].Name == "room_contents" {
|
||||
changed[i].Value = rest
|
||||
}
|
||||
}
|
||||
p.logf("decor: kept %d community products out of %s saved to IMVU", stripped, instance)
|
||||
return p.replaceBody(x, &xmlrpc.Call{Method: call.Method, Params: []xmlrpc.Value{changed}})
|
||||
}
|
||||
|
||||
func (p *Products) interceptGetRoomState(x *proxy.Exchange, call *xmlrpc.Call) error {
|
||||
args, ok := call.Params[0].(xmlrpc.Struct)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
instance := member(args, "room_instance_id")
|
||||
if instance == "" || isHostedInstance(instance) {
|
||||
return nil
|
||||
}
|
||||
// Asked of the hub while IMVU is asked about the room: no added wait.
|
||||
got := make(chan []DecorItem, 1)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
got <- p.decorFetch(ctx, instance)
|
||||
}()
|
||||
x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) {
|
||||
r, err := xmlrpc.ParseResponse(body)
|
||||
if err != nil || r.Fault != nil || len(r.Params) != 1 {
|
||||
return body, nil
|
||||
}
|
||||
state, _ := r.Params[0].(xmlrpc.Struct)
|
||||
roomState, _ := state.Get("room_state")
|
||||
rs, ok := roomState.(xmlrpc.Struct)
|
||||
if !ok {
|
||||
return body, nil
|
||||
}
|
||||
v, _ := rs.Get("room_contents")
|
||||
rows, _ := v.([]xmlrpc.Value)
|
||||
info, _ := rs.Get("room_info")
|
||||
p.decor.setBase(instance, rows, infoMap(info))
|
||||
var items []DecorItem
|
||||
select {
|
||||
case items = <-got:
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
extra := p.decorLocals(items)
|
||||
if len(extra) == 0 {
|
||||
return body, nil
|
||||
}
|
||||
for i := range rs {
|
||||
if rs[i].Name == "room_contents" {
|
||||
rs[i].Value = append(append([]xmlrpc.Value{}, rows...), extra...)
|
||||
}
|
||||
}
|
||||
return r.Marshal(), nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// member is a struct member as a string (strings and numbers).
|
||||
func member(s xmlrpc.Struct, name string) string {
|
||||
v, ok := s.Get(name)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- the owner's broadcast in the room's chat ---
|
||||
|
||||
const setRoomStateCommand = "*imvu:setRoomState "
|
||||
|
||||
// decodeScene reads a broadcast scene: "c" and base64 of zlib-compressed JSON (the client's
|
||||
// RoomState.encodeRoomState).
|
||||
func decodeScene(encoded string) (map[string]any, bool) {
|
||||
encoded = strings.TrimLeft(encoded, " \t\r\n")
|
||||
if !strings.HasPrefix(encoded, "c") {
|
||||
return nil, false
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.Map(func(r rune) rune {
|
||||
if r == '\n' || r == '\r' || r == ' ' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, encoded[1:]))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
zr, err := zlib.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
b, err := io.ReadAll(io.LimitReader(zr, 4<<20))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
d := json.NewDecoder(bytes.NewReader(b))
|
||||
d.UseNumber()
|
||||
var scene map[string]any
|
||||
if d.Decode(&scene) != nil {
|
||||
return nil, false
|
||||
}
|
||||
return scene, true
|
||||
}
|
||||
|
||||
// encodeScene writes a scene as the client does (base64 in lines of 76, each ending in a newline).
|
||||
func encodeScene(scene map[string]any) string {
|
||||
b, _ := json.Marshal(scene)
|
||||
var z bytes.Buffer
|
||||
w := zlib.NewWriter(&z)
|
||||
w.Write(b)
|
||||
w.Close()
|
||||
s := base64.StdEncoding.EncodeToString(z.Bytes())
|
||||
var out strings.Builder
|
||||
out.WriteString("c")
|
||||
for len(s) > 76 {
|
||||
out.WriteString(s[:76])
|
||||
out.WriteString("\n")
|
||||
s = s[76:]
|
||||
}
|
||||
out.WriteString(s)
|
||||
out.WriteString("\n")
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// sceneContents is the scene's room_contents and where it is (at the top, or under room_state).
|
||||
func sceneContents(scene map[string]any) (map[string]any, []any) {
|
||||
holder := scene
|
||||
if rs, ok := scene["room_state"].(map[string]any); ok {
|
||||
holder = rs
|
||||
}
|
||||
rows, _ := holder["room_contents"].([]any)
|
||||
return holder, rows
|
||||
}
|
||||
|
||||
func sceneInstance(scene map[string]any) string {
|
||||
holder, _ := sceneContents(scene)
|
||||
info, _ := holder["room_info"].(map[string]any)
|
||||
switch v := info["room_instance_id"].(type) {
|
||||
case string:
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// outgoingScene takes community products out of the owner's broadcast (keeping them as the room's decorations).
|
||||
func (p *Products) outgoingScene(m *chatMessage) ([]byte, bool) {
|
||||
scene, ok := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
holder, rows := sceneContents(scene)
|
||||
instance := sceneInstance(scene)
|
||||
rest, items, stripped := p.splitCommunity(rows)
|
||||
_, hadDecor := p.decor.get(instance)
|
||||
if instance != "" && !isHostedInstance(instance) && (stripped > 0 || hadDecor) {
|
||||
p.saveDecor(instance, items)
|
||||
}
|
||||
if instance != "" {
|
||||
p.decor.setBase(instance, rest, holder["room_info"])
|
||||
}
|
||||
if stripped == 0 {
|
||||
return nil, false
|
||||
}
|
||||
if rest == nil {
|
||||
rest = []any{}
|
||||
}
|
||||
holder["room_contents"] = rest
|
||||
p.logf("decor: kept %d community products out of a room broadcast", stripped)
|
||||
return m.withText(setRoomStateCommand + encodeScene(scene)), true
|
||||
}
|
||||
|
||||
// incomingScene adds the room's decorations to its owner's broadcast.
|
||||
func (p *Products) incomingScene(m *chatMessage) ([]byte, bool) {
|
||||
scene, ok := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
instance := sceneInstance(scene)
|
||||
if instance == "" || isHostedInstance(instance) {
|
||||
return nil, false
|
||||
}
|
||||
holder, rows := sceneContents(scene)
|
||||
p.decor.setBase(instance, rows, holder["room_info"])
|
||||
items, _ := p.decor.get(instance)
|
||||
extra := p.decorLocals(items)
|
||||
if len(extra) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
holder["room_contents"] = append(append([]any{}, rows...), extra...)
|
||||
return m.withText(setRoomStateCommand + encodeScene(scene)), true
|
||||
}
|
||||
|
||||
// DecorChanged is told by the hub that a room's decorations changed: they're asked for again, and if the client is
|
||||
// in that room, it gets the room again with them (as its owner's broadcast).
|
||||
func (p *Products) DecorChanged(instance string) {
|
||||
p.decor.mu.Lock()
|
||||
delete(p.decor.rooms, instance)
|
||||
p.decor.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
items := p.decorFetch(ctx, instance)
|
||||
if roomOwner(instance) == p.User() {
|
||||
return // the owner's client has the room as it is
|
||||
}
|
||||
base, info := p.decor.getBase(instance)
|
||||
if base == nil || info == nil {
|
||||
return
|
||||
}
|
||||
p.roomMu.Lock()
|
||||
p.room.init()
|
||||
var chat string
|
||||
for c, inst := range p.room.rooms {
|
||||
if inst == instance && p.room.inChat(c) {
|
||||
chat = c
|
||||
}
|
||||
}
|
||||
var conns []*proxy.IMQConn
|
||||
for c, chats := range p.room.subs {
|
||||
if chats[chat] {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
}
|
||||
p.roomMu.Unlock()
|
||||
if chat == "" {
|
||||
return
|
||||
}
|
||||
owner := roomOwner(instance)
|
||||
scene := map[string]any{"room_state": map[string]any{
|
||||
"room_info": info,
|
||||
"room_contents": append(append([]any{}, base...), p.decorLocals(items)...),
|
||||
}}
|
||||
chatID, _ := strconv.ParseInt(chat, 10, 64)
|
||||
msg, _ := json.Marshal(map[string]any{"userId": owner, "chatId": chatID,
|
||||
"message": setRoomStateCommand + encodeScene(scene), "to": 0})
|
||||
for _, c := range conns {
|
||||
c.Deliver(imq.Delivered{UserID: "/user/" + strconv.FormatInt(owner, 10), Queue: "/chat/" + chat, Mount: "messages", Message: msg})
|
||||
}
|
||||
p.logf("decor: gave the client %s again with its decorations", instance)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package products
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/imq"
|
||||
"git.t5de.dev/t5de/t5de-proxy/internal/xmlrpc"
|
||||
)
|
||||
|
||||
func TestSceneEncoding(t *testing.T) {
|
||||
scene := map[string]any{"room_state": map[string]any{"room_contents": []any{[]any{1, 2, "x"}}}}
|
||||
back, ok := decodeScene(encodeScene(scene))
|
||||
if !ok {
|
||||
t.Fatal("didn't decode")
|
||||
}
|
||||
if _, rows := sceneContents(back); len(rows) != 1 {
|
||||
t.Fatalf("rows %v", rows)
|
||||
}
|
||||
if enc := encodeScene(scene); !strings.HasPrefix(enc, "c") || !strings.HasSuffix(enc, "\n") {
|
||||
t.Fatalf("encoded %q", enc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecorations(t *testing.T) {
|
||||
h, _ := newHarness(t)
|
||||
hubDir := t.TempDir()
|
||||
writePack(t, hubDir, "sofasofa22", 1)
|
||||
h.p.dirs = append(h.p.dirs, hubDir)
|
||||
h.p.HubDir = hubDir
|
||||
h.p.refresh(true)
|
||||
sofa := h.p.byProductID("sofasofa22").local
|
||||
h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false)
|
||||
|
||||
var mu sync.Mutex
|
||||
stored := map[string][]DecorItem{}
|
||||
saved := make(chan string, 4)
|
||||
h.p.Decorations = func(ctx context.Context, instance string) ([]DecorItem, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return stored[instance], nil
|
||||
}
|
||||
h.p.SaveDecorations = func(ctx context.Context, instance string, items []DecorItem) error {
|
||||
mu.Lock()
|
||||
stored[instance] = items
|
||||
mu.Unlock()
|
||||
saved <- instance
|
||||
return nil
|
||||
}
|
||||
|
||||
// The owner saves the room: IMVU gets it without the community sofa, the hub gets the sofa.
|
||||
save := xmlrpc.Struct{{Name: "room_instance_id", Value: "7-1"}, {Name: "room_contents", Value: []xmlrpc.Value{
|
||||
[]xmlrpc.Value{int64(10), int64(301), "2-1"}, []xmlrpc.Value{int64(11), sofa, fmt.Sprintf("3 0 11 %d 632", sofa)}}}}
|
||||
h.do("POST", "http://client-dynamic.imvu.com/api/xmlrpc/client.php", call("test.setRoomState", save), true, false)
|
||||
if asked := h.imvu.asked["test.setRoomState"]; len(asked) != 1 || strings.Contains(asked[0], fmt.Sprint(sofa)) || !strings.Contains(asked[0], "301") {
|
||||
t.Fatalf("IMVU got %v", asked)
|
||||
}
|
||||
if <-saved != "7-1" || fmt.Sprint(stored["7-1"]) != "[{11 sofasofa22 3 0 11 sofasofa22 632}]" {
|
||||
t.Fatalf("stored %v", stored)
|
||||
}
|
||||
|
||||
// Loading the room (anyone): the sofa is added back.
|
||||
h.p.decor = decorations{} // as if another computer
|
||||
_, body := h.do("POST", "http://client-dynamic.imvu.com/api/xmlrpc/client.php", call("test.getRoomState", xmlrpc.Struct{{Name: "room_instance_id", Value: "7-1"}}), true, false)
|
||||
if !strings.Contains(string(body), fmt.Sprintf("3 0 11 %d 632", sofa)) || !strings.Contains(string(body), "301") {
|
||||
t.Fatalf("loaded %s", body)
|
||||
}
|
||||
|
||||
// The owner's broadcast: the sofa is taken out for IMVU...
|
||||
scene := map[string]any{"room_state": map[string]any{
|
||||
"room_info": map[string]any{"customers_id": "7", "room_instance_id": "7-1", "room_pid": "300"},
|
||||
"room_contents": []any{[]any{10, 301, "2-1"}, []any{11, sofa, "2-2"}}}}
|
||||
msg, _ := json.Marshal(map[string]any{"userId": 7, "chatId": 555, "message": setRoomStateCommand + encodeScene(scene), "to": 0})
|
||||
out, drop := h.p.Outgoing("/chat/555", "messages", msg)
|
||||
if drop {
|
||||
t.Fatal("dropped")
|
||||
}
|
||||
m, _ := parseChatMessage(out)
|
||||
sent, _ := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||
if _, rows := sceneContents(sent); len(rows) != 1 {
|
||||
t.Fatalf("sent to IMVU: %v", rows)
|
||||
}
|
||||
// ...and added back for whoever gets it with T5DE.
|
||||
in := h.p.Incoming(nil, imq.Delivered{UserID: "/user/7", Queue: "/chat/555", Mount: "messages", Message: out})
|
||||
m, _ = parseChatMessage(in)
|
||||
got, _ := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||
if _, rows := sceneContents(got); len(rows) != 2 {
|
||||
t.Fatalf("delivered: %v", rows)
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,14 @@ func (p *Products) interceptXMLRPC(x *proxy.Exchange) error {
|
||||
x.ModifyResponse = func(resp *http.Response, body []byte) ([]byte, error) {
|
||||
return p.addToInventory(body)
|
||||
}
|
||||
case "test.setRoomState":
|
||||
if len(call.Params) == 1 {
|
||||
return p.interceptSetRoomState(x, call)
|
||||
}
|
||||
case "test.getRoomState":
|
||||
if len(call.Params) == 1 {
|
||||
return p.interceptGetRoomState(x, call)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -577,6 +585,12 @@ func (p *Products) outgoing(queue, mount string, message []byte) ([]byte, bool)
|
||||
if !ok {
|
||||
return message, false
|
||||
}
|
||||
if strings.HasPrefix(m.text, setRoomStateCommand) {
|
||||
if out, changed := p.outgoingScene(m); changed {
|
||||
return out, false
|
||||
}
|
||||
return message, false
|
||||
}
|
||||
r := stripCommand(m.text, IsCommunity)
|
||||
if r.outfit && m.userID != 0 {
|
||||
p.recordWorn(strconv.FormatInt(m.userID, 10), r.kept, r.products)
|
||||
|
||||
@@ -68,6 +68,10 @@ type Products struct {
|
||||
AddToLibrary func(ctx context.Context, id string) error
|
||||
// Library, if set, is the person's hub library as last read (for the inventory's Recently Added).
|
||||
Library func() []hub.LibraryEntry
|
||||
// Decorations and SaveDecorations, if set, get and store an IMVU room's decorations on the hub (decor.go).
|
||||
Decorations func(ctx context.Context, instance string) ([]DecorItem, error)
|
||||
SaveDecorations func(ctx context.Context, instance string, items []DecorItem) error
|
||||
decor decorations
|
||||
|
||||
// Hosted, if set, serves T5DE-hosted rooms' queues (the hosted package); HostedOwner is the IMVU account owning
|
||||
// the hosted room the client is in (0 if none), whose products the room's are.
|
||||
|
||||
@@ -125,6 +125,14 @@ func (f *fakeIMVU) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{
|
||||
{Name: "userId", Value: int64(7)}, {Name: "securityKey", Value: key},
|
||||
{Name: "look", Value: []xmlrpc.Value{int64(80), int64(1)}}}}}).Marshal()))
|
||||
case "test.setRoomState":
|
||||
checkSigned(false)
|
||||
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{{Name: "state_changed", Value: true}, {Name: "room_hash", Value: "h"}}}}).Marshal()))
|
||||
case "test.getRoomState":
|
||||
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{xmlrpc.Struct{{Name: "room_state", Value: xmlrpc.Struct{
|
||||
{Name: "room_info", Value: xmlrpc.Struct{{Name: "customers_id", Value: "7"}, {Name: "room_instance_id", Value: "7-1"}, {Name: "room_pid", Value: "300"}}},
|
||||
{Name: "room_contents", Value: []xmlrpc.Value{[]xmlrpc.Value{int64(10), int64(301), "2-1"}}}}},
|
||||
{Name: "room_hash", Value: "h"}}}}).Marshal()))
|
||||
case "test.showInventory":
|
||||
return reply(string((&xmlrpc.Response{Params: []xmlrpc.Value{[]xmlrpc.Value{int64(1), int64(2)}}}).Marshal()))
|
||||
case "test.inventoryProductBatch":
|
||||
|
||||
@@ -153,6 +153,14 @@ func (p *Products) Incoming(c *proxy.IMQConn, m imq.Delivered) []byte {
|
||||
if user == 0 {
|
||||
return m.Message
|
||||
}
|
||||
if bytes.Contains(m.Message, []byte(setRoomStateCommand)) {
|
||||
if msg, ok := parseChatMessage(m.Message); ok && strings.HasPrefix(msg.text, setRoomStateCommand) {
|
||||
if out, changed := p.incomingScene(msg); changed {
|
||||
return out
|
||||
}
|
||||
return m.Message
|
||||
}
|
||||
}
|
||||
p.roomMu.Lock()
|
||||
p.room.init()
|
||||
p.room.userIDs[user] = m.UserID
|
||||
|
||||
Reference in new issue
Block a user