Room channel: other T5DE users' community products on their avatars
CI / build (push) Successful in 28s

The proxy follows the client's IMQ chat subscriptions and tells the hub which chats it's in (and the room, from
chat.getOrMakeChat). For the other T5DE users the hub says are in a chat, it gives the client the community
products it has packs for, as *use and *remove lines from them, and adds them to the whole outfits they send.
It also takes community products out of the outfit the client posts to IMVU's chat participant endpoint, which
had been sending them to IMVU.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 18:14:44 -07:00
1 parent e394f1dc24
commit 93fa2f5d90
11 files changed
+858 -12

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
package products
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
"git.t5de.dev/t5de/t5de-proxy/internal/imq"
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
"git.t5de.dev/t5de/t5de-proxy/internal/xmlrpc"
)
// fakeObserver records what the hub client would be told.
type fakeObserver struct {
mu sync.Mutex
events []string
}
func (f *fakeObserver) add(e string) {
f.mu.Lock()
defer f.mu.Unlock()
f.events = append(f.events, e)
}
func (f *fakeObserver) Session(int64) {}
func (f *fakeObserver) Outfit(int64, []hub.Item) {}
func (f *fakeObserver) Join(chat, room string) { f.add("join " + chat + " " + room) }
func (f *fakeObserver) Leave(chat string) { f.add("leave " + chat) }
// delivered reads the chat lines delivered to a client: "from: text".
func delivered(t *testing.T, b *bytes.Buffer) []string {
t.Helper()
var lines []string
for b.Len() > 0 {
f, n, err := imq.Next(b.Bytes())
if err != nil || n == 0 {
t.Fatalf("bad frames: %v", err)
}
b.Next(n)
m, err := imq.ParseDelivered(f.Data)
if err != nil || m.Queue != "/chat/555" || m.Mount != "messages" {
t.Fatalf("delivered %+v %v", m, err)
}
var msg struct {
Message string
UserID int64 `json:"userId"`
}
json.Unmarshal(m.Message, &msg)
lines = append(lines, fmt.Sprintf("%s(%d): %s", m.UserID, msg.UserID, msg.Message))
}
return lines
}
func TestRoomChannel(t *testing.T) {
h, _ := newHarness(t)
obs := &fakeObserver{}
h.p.Observer = obs
local := h.p.byProductID("blue-coat").local
// Joining a room: getOrMakeChat says which chat it is, then the client subscribes to its queue.
h.do("POST", "http://chat.imvu.com/api/xmlrpc/chat.php", call("chat.getOrMakeChat",
xmlrpc.Struct{{Name: "userId", Value: int64(7)}, {Name: "activity", Value: "publicroom-12-3"}}), false, false)
var toClient bytes.Buffer
c := proxy.NewIMQConn(1, &toClient)
h.p.Subscribed(c, []string{"/user/7", "/chat/555"})
if got := strings.Join(obs.events, "; "); got != "join 555 12-3" {
t.Fatalf("observer heard %q", got)
}
// Someone else wears the coat (and something not in the packs): the client is given it from them.
h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: []byte(`{"message":"hi","userId":88}`)})
h.p.Others("555", []hub.Avatar{{IMVUID: 88, Items: []hub.Item{{ID: "blue-coat", Name: "Coat"}, {ID: "elsewhere", Name: "Hat"}}}})
if got := delivered(t, &toClient); len(got) != 1 || got[0] != fmt.Sprintf("/user/88(88): *use %d", local) {
t.Fatalf("delivered %q", got)
}
// Told again with no change: nothing new.
h.p.Others("555", []hub.Avatar{{IMVUID: 88, Items: []hub.Item{{ID: "blue-coat", Name: "Coat"}}}})
if toClient.Len() != 0 {
t.Fatal("delivered again")
}
// Their whole outfit (which IMVU sends without it) gets it added; other lines don't.
whole := []byte(`{"message":"*use 80 1 2","userId":88,"chatId":555,"to":0}`)
out := h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: whole})
if !bytes.Contains(out, []byte(fmt.Sprintf(`"message":"*use 80 1 2 %d"`, local))) || !bytes.Contains(out, []byte(`"chatId":555`)) {
t.Fatalf("whole outfit %s", out)
}
part := []byte(`{"message":"*use 3","userId":88}`)
if out := h.p.Incoming(c, imq.Delivered{UserID: "/user/88", Queue: "/chat/555", Mount: "messages", Message: part}); !bytes.Equal(out, part) {
t.Fatalf("a single product changed: %s", out)
}
other := []byte(`{"message":"*use 80 1","userId":99}`)
if out := h.p.Incoming(c, imq.Delivered{UserID: "/user/99", Queue: "/chat/555", Mount: "messages", Message: other}); !bytes.Equal(out, other) {
t.Fatalf("someone else's outfit changed: %s", out)
}
// They take it off, then leave: the client is told to take it off once.
h.p.Others("555", []hub.Avatar{{IMVUID: 88}})
h.p.Others("555", []hub.Avatar{})
if got := delivered(t, &toClient); len(got) != 1 || got[0] != fmt.Sprintf("/user/88(88): *remove %d", local) {
t.Fatalf("delivered %q", got)
}
// The connection closing leaves the chat.
h.p.Closed(c)
if got := obs.events[len(obs.events)-1]; got != "leave 555" {
t.Fatalf("last event %q", got)
}
}
func TestParticipantPostKeepsCommunityProducts(t *testing.T) {
h, _ := newHarness(t)
local := h.p.byProductID("blue-coat").local
h.do("POST", "https://secure.imvu.com/api/xmlrpc/client.php", call("test.avatarInfoForLogin2"), false, false)
body := []byte(fmt.Sprintf(`{"legacy_outfit_message": "*use 80 1 %d", "seat_number": 2}`, local))
if status, _ := h.do("POST", "https://api.imvu.com/chat/chat-555/participants/user-7", body, true, false); status != 200 {
t.Fatalf("status %d", status)
}
got := h.imvu.asked["participant"]
if len(got) != 1 || strings.Contains(got[0], fmt.Sprint(local)) || !strings.Contains(got[0], `"legacy_outfit_message":"*use 80 1"`) || !strings.Contains(got[0], `"seat_number":2`) {
t.Fatalf("IMVU was sent %q", got)
}
}