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
|
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) {
|
func (d *decorations) set(instance string, items []DecorItem) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
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 {
|
if items, ok := p.decor.get(instance); ok {
|
||||||
return items
|
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 {
|
if p.Decorations == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
items, err := p.Decorations(ctx, instance)
|
items, err := p.Decorations(ctx, instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logf("decor: %s: %v", instance, err)
|
p.logf("decor: %s: %v", instance, err)
|
||||||
return nil
|
old, _ := p.decor.last(instance)
|
||||||
|
return old
|
||||||
}
|
}
|
||||||
p.decor.set(instance, items)
|
p.decor.set(instance, items)
|
||||||
return items
|
return items
|
||||||
@@ -457,36 +471,107 @@ func (p *Products) outgoingScene(m *chatMessage) ([]byte, bool) {
|
|||||||
return m.withText(setRoomStateCommand + encodeScene(scene)), true
|
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.
|
// incomingScene adds the room's decorations to its owner's broadcast.
|
||||||
func (p *Products) incomingScene(m *chatMessage) ([]byte, bool) {
|
func (p *Products) incomingScene(m *chatMessage) ([]byte, bool) {
|
||||||
scene, ok := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
scene, ok := decodeScene(strings.TrimPrefix(m.text, setRoomStateCommand))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
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)
|
instance := sceneInstance(scene)
|
||||||
if instance == "" || isHostedInstance(instance) {
|
if instance == "" || isHostedInstance(instance) {
|
||||||
return nil, false
|
return "", false
|
||||||
}
|
}
|
||||||
holder, rows := sceneContents(scene)
|
holder, rows := sceneContents(scene)
|
||||||
p.decor.setBase(instance, rows, holder["room_info"])
|
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)
|
extra := p.decorLocals(items)
|
||||||
if len(extra) == 0 {
|
if len(extra) == 0 {
|
||||||
return nil, false
|
return "", false
|
||||||
}
|
}
|
||||||
holder["room_contents"] = append(append([]any{}, rows...), extra...)
|
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
|
// 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).
|
// in that room, it gets the room again with them (as its owner's broadcast).
|
||||||
func (p *Products) DecorChanged(instance string) {
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
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() {
|
if roomOwner(instance) == p.User() {
|
||||||
return // the owner's client has the room as it is
|
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)
|
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) {
|
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" {
|
if !strings.HasPrefix(queue, "/chat/") || mount != "messages" {
|
||||||
return message, false
|
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.
|
// 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 {
|
func (p *Products) Incoming(c *proxy.IMQConn, m imq.Delivered) []byte {
|
||||||
chat := chatOf(m.Queue)
|
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" {
|
if chat == "" || m.Mount != "messages" {
|
||||||
return m.Message
|
return m.Message
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user