After the owner saves, IMVU's server sends everyone the saved room ({"command": "SetRoomState"} from
admin on the chat's control mount), which has no decorations: the proxy now adds them to it, and takes
community products out of any outgoing one. A change notice from the hub also no longer empties the
remembered decorations while the new ones are asked for (a broadcast arriving meanwhile went through
without them).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0be2cd8f4e
commit
13ee023836
4 files changed
+159
-9
No files matched your search
@@ -75,6 +75,14 @@ func (d *decorations) get(instance string) ([]DecorItem, bool) {
|
||||
return e.items, true
|
||||
}
|
||||
|
||||
// last is the room's decorations as last known, however old (a broadcast can't wait for the hub).
|
||||
func (d *decorations) last(instance string) ([]DecorItem, bool) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
e, ok := d.rooms[instance]
|
||||
return e.items, ok
|
||||
}
|
||||
|
||||
func (d *decorations) set(instance string, items []DecorItem) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
@@ -135,13 +143,19 @@ func (p *Products) decorFetch(ctx context.Context, instance string) []DecorItem
|
||||
if items, ok := p.decor.get(instance); ok {
|
||||
return items
|
||||
}
|
||||
return p.decorRefresh(ctx, instance)
|
||||
}
|
||||
|
||||
// decorRefresh asks the hub for a room's decorations; what was known stays until the answer replaces it.
|
||||
func (p *Products) decorRefresh(ctx context.Context, instance string) []DecorItem {
|
||||
if p.Decorations == nil {
|
||||
return nil
|
||||
}
|
||||
items, err := p.Decorations(ctx, instance)
|
||||
if err != nil {
|
||||
p.logf("decor: %s: %v", instance, err)
|
||||
return nil
|
||||
old, _ := p.decor.last(instance)
|
||||
return old
|
||||
}
|
||||
p.decor.set(instance, items)
|
||||
return items
|
||||
@@ -457,36 +471,107 @@ func (p *Products) outgoingScene(m *chatMessage) ([]byte, bool) {
|
||||
return m.withText(setRoomStateCommand + encodeScene(scene)), true
|
||||
}
|
||||
|
||||
// incomingControlScene adds the room's decorations to IMVU's server's {"command": "SetRoomState", "state": scene}
|
||||
// on a chat's control mount.
|
||||
func (p *Products) incomingControlScene(message []byte) ([]byte, bool) {
|
||||
if !bytes.Contains(message, []byte(`"SetRoomState"`)) {
|
||||
return nil, false
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if json.Unmarshal(message, &fields) != nil {
|
||||
return nil, false
|
||||
}
|
||||
var command, state string
|
||||
if json.Unmarshal(fields["command"], &command) != nil || command != "SetRoomState" || json.Unmarshal(fields["state"], &state) != nil {
|
||||
return nil, false
|
||||
}
|
||||
scene, ok := decodeScene(state)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
encoded, changed := p.decorateScene(scene)
|
||||
if !changed {
|
||||
return nil, false
|
||||
}
|
||||
fields["state"], _ = json.Marshal(encoded)
|
||||
out, err := json.Marshal(fields)
|
||||
return out, err == nil
|
||||
}
|
||||
|
||||
// outgoingControlScene takes community products out of a SetRoomState the client sends on a control mount (it
|
||||
// isn't known to, but nothing naming them goes to IMVU).
|
||||
func (p *Products) outgoingControlScene(message []byte) ([]byte, bool) {
|
||||
if !bytes.Contains(message, []byte(`"SetRoomState"`)) {
|
||||
return nil, false
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
var state string
|
||||
if json.Unmarshal(message, &fields) != nil || json.Unmarshal(fields["state"], &state) != nil {
|
||||
return nil, false
|
||||
}
|
||||
scene, ok := decodeScene(state)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
holder, rows := sceneContents(scene)
|
||||
rest, _, stripped := p.splitCommunity(rows)
|
||||
if stripped == 0 {
|
||||
return nil, false
|
||||
}
|
||||
if rest == nil {
|
||||
rest = []any{}
|
||||
}
|
||||
holder["room_contents"] = rest
|
||||
fields["state"], _ = json.Marshal(encodeScene(scene))
|
||||
out, err := json.Marshal(fields)
|
||||
p.logf("decor: kept %d community products out of a room state sent to IMVU", stripped)
|
||||
return out, err == nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
encoded, changed := p.decorateScene(scene)
|
||||
if !changed {
|
||||
return nil, false
|
||||
}
|
||||
return m.withText(setRoomStateCommand + encoded), true
|
||||
}
|
||||
|
||||
// decorateScene adds the room's decorations to a scene IMVU sent: the scene encoded again, and whether it changed.
|
||||
func (p *Products) decorateScene(scene map[string]any) (string, bool) {
|
||||
instance := sceneInstance(scene)
|
||||
if instance == "" || isHostedInstance(instance) {
|
||||
return nil, false
|
||||
return "", false
|
||||
}
|
||||
holder, rows := sceneContents(scene)
|
||||
p.decor.setBase(instance, rows, holder["room_info"])
|
||||
items, _ := p.decor.get(instance)
|
||||
items, fresh := p.decor.get(instance)
|
||||
if !fresh {
|
||||
items, _ = p.decor.last(instance) // stale or none: used now, asked for again for next time
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
p.decorRefresh(ctx, instance)
|
||||
}()
|
||||
}
|
||||
extra := p.decorLocals(items)
|
||||
if len(extra) == 0 {
|
||||
return nil, false
|
||||
return "", false
|
||||
}
|
||||
holder["room_contents"] = append(append([]any{}, rows...), extra...)
|
||||
return m.withText(setRoomStateCommand + encodeScene(scene)), true
|
||||
return 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)
|
||||
items := p.decorRefresh(ctx, instance) // the old ones stay in use until then
|
||||
if roomOwner(instance) == p.User() {
|
||||
return // the owner's client has the room as it is
|
||||
}
|
||||
|
||||
@@ -92,3 +92,55 @@ func TestDecorations(t *testing.T) {
|
||||
t.Fatalf("delivered: %v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// A broadcast arriving while the proxy asks the hub for changed decorations still gets the ones known so far.
|
||||
func TestDecorationsWhileRefreshing(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
|
||||
release := make(chan struct{})
|
||||
asked := make(chan struct{}, 1)
|
||||
h.p.Decorations = func(ctx context.Context, instance string) ([]DecorItem, error) {
|
||||
asked <- struct{}{}
|
||||
<-release
|
||||
return []DecorItem{{11, "sofasofa22", "x"}, {12, "sofasofa22", "y"}}, nil
|
||||
}
|
||||
h.p.decor.set("7-1", []DecorItem{{11, "sofasofa22", "x"}})
|
||||
go h.p.DecorChanged("7-1")
|
||||
<-asked // the hub is being asked
|
||||
scene := map[string]any{"room_state": map[string]any{
|
||||
"room_info": map[string]any{"room_instance_id": "7-1"}, "room_contents": []any{[]any{10, 301, "2-1"}}}}
|
||||
msg, _ := json.Marshal(map[string]any{"userId": 7, "chatId": 555, "message": setRoomStateCommand + encodeScene(scene)})
|
||||
in := h.p.Incoming(nil, imq.Delivered{UserID: "7", Queue: "/chat/555", Mount: "messages", Message: msg})
|
||||
close(release)
|
||||
m, _ := parseChatMessage(in)
|
||||
got, _ := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||
if _, rows := sceneContents(got); len(rows) != 2 || !strings.Contains(fmt.Sprint(rows), fmt.Sprint(sofa)) {
|
||||
t.Fatalf("delivered %v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// IMVU's server sends a saved room on the chat's control mount; decorations are added to it too.
|
||||
func TestDecorationsInServerRoomState(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)
|
||||
h.p.decor.set("7-1", []DecorItem{{11, "sofasofa22", "x"}})
|
||||
scene := map[string]any{"room_state": map[string]any{
|
||||
"room_info": map[string]any{"room_instance_id": "7-1"}, "room_contents": []any{[]any{10, 301, "2-1"}}}}
|
||||
msg, _ := json.Marshal(map[string]any{"command": "SetRoomState", "state": encodeScene(scene)})
|
||||
in := h.p.Incoming(nil, imq.Delivered{UserID: "admin", Queue: "/chat/555", Mount: "control", Message: msg})
|
||||
var fields map[string]string
|
||||
json.Unmarshal(in, &fields)
|
||||
got, _ := decodeScene(fields["state"])
|
||||
if _, rows := sceneContents(got); len(rows) != 2 || fields["command"] != "SetRoomState" {
|
||||
t.Fatalf("delivered %s", in)
|
||||
}
|
||||
}
|
||||
@@ -578,6 +578,12 @@ func (p *Products) Outgoing(queue, mount string, message []byte) ([]byte, bool)
|
||||
}
|
||||
|
||||
func (p *Products) outgoing(queue, mount string, message []byte) ([]byte, bool) {
|
||||
if strings.HasPrefix(queue, "/chat/") && mount == "control" {
|
||||
if out, changed := p.outgoingControlScene(message); changed {
|
||||
return out, false
|
||||
}
|
||||
return message, false
|
||||
}
|
||||
if !strings.HasPrefix(queue, "/chat/") || mount != "messages" {
|
||||
return message, false
|
||||
}
|
||||
|
||||
@@ -146,6 +146,13 @@ func userOf(userID string) int64 {
|
||||
// Incoming implements proxy.IMQWatcher: a T5DE user's whole outfit gets their community products added.
|
||||
func (p *Products) Incoming(c *proxy.IMQConn, m imq.Delivered) []byte {
|
||||
chat := chatOf(m.Queue)
|
||||
if chat != "" && m.Mount == "control" {
|
||||
// IMVU's server sends everyone a room its owner saved (as IMVU has it: without decorations).
|
||||
if out, changed := p.incomingControlScene(m.Message); changed {
|
||||
return out
|
||||
}
|
||||
return m.Message
|
||||
}
|
||||
if chat == "" || m.Mount != "messages" {
|
||||
return m.Message
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user