From 355002ec793207e6378831c3ea46ddcbe9d114b2 Mon Sep 17 00:00:00 2001 From: dhkatz Date: Thu, 29 Oct 2026 02:10:42 -0700 Subject: [PATCH] What a room needs from the server, for hosted rooms Co-Authored-By: Claude Opus 5.5 --- docs/imq-rooms.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/imq-rooms.md diff --git a/docs/imq-rooms.md b/docs/imq-rooms.md new file mode 100644 index 0000000..e46190a --- /dev/null +++ b/docs/imq-rooms.md @@ -0,0 +1,118 @@ +# What a room needs from the server (client 554.8) + +Findings for T5DE-hosted rooms (design.md): what the client asks and expects when it joins and uses a chat room, so +the hub (through the proxy) can play IMVU's part for rooms it hosts. From the client's code and a capture of the test +account joining an empty public room, chatting once and leaving (2026-10-29). Our own description; IDs are +placeholders. + +## Two channels + +A room uses both of the client's connections to IMVU: + +- **XML-RPC** (`chat.imvu.com/api/xmlrpc/chat.php` for `chat.*`, `client-dynamic.imvu.com/api/xmlrpc/client.php` + for `test.*`): joining, the room's scene, the participant list, leaving. +- **IMQ**, the real-time connection (one per client, through the proxy as plaintext): everything said and done in + the room, as messages on a queue per chat. + +## Joining a room + +In order, as captured: + +1. `test.getRoomCardInfo({room_instance_id})`: the room's card (name, owner, flags like `allowApProducts`, + `allowLoadNewRoom`). Room instance IDs look like `-`. +2. `chat.getOrMakeChat({userId, activity: "publicroom-", chatId: 0, publicroom: true, + private})`: returns `chatId` (an integer), `seat`, `lastMessageId`, `participantUrl`, `partnerId`. A "declined" + response carries `reason` and `explanation` (shown as an error). A "waiting" response makes the client wait and + retry with `inviteId`. +3. `test.getRoomState({room_instance_id})`: the scene: `room_hash`, and `room_state.room_contents`, a list of + `[furniture instance ID, product ID, placement]`, where placement is a string of numbers (position, rotation, + scale, parent slot...). The client caches it by hash. +4. IMQ: subscribe to `/chat/` (with property `listen_to_self: false`), then `inv:/room/room-` (contents not seen yet). +5. Chat commands sent on joining (below): `*msg SeatAssignment ...`, `*seat `, `*resume ` (or + `*accept `). +6. `chat.getParticipants({chatId, userId})`, then again about every 15 seconds: `[{userId, seat, ...}]`. People + join and leave in the client when they appear in or drop out of this list; messages from people not in it are + ignored. + +Leaving: `chat.terminateChat(userId, chatId)`. Booting: `chat.bootOutOfChat({userId, ...})`. Invitations: +`chat.attemptInvite`, `acceptInvite`, `declineInvite`, `leaveInviteAwayNote`. Also seen: `test.getBuddyState`, +`test.updateAvatarDownloadSize`. + +The room list subscribes to `room_occupancy/` for each room shown (mount `occupants`), for live +occupancy counts. + +## IMQ + +Frames are protobuf `Framing{type, data}` back to back (t5de-proxy `internal/imq`). Message types and fields, as the +client defines them (field numbers in brackets): + +| Type | Fields | +| --- | --- | +| C2gConnect 1302 | version [1], user_id [2] (string), cookie [3] (bytes, from login), op_id [4], metadata [5] (key, value) | +| G2cChallenge 3103 | version [1], challenge [2] | +| C2gChallengeResponse 1303 | op_id [1], response [2] (from the challenge and an auth token from login) | +| G2cResult 3102 | op_id [1], status [2], error_message [3]: the answer to any operation with an op_id | +| C2gOpenFloodgates 1306 | state [1]: (queue [1], got_sequence [2], op_id [3]) per queue, to resume after reconnecting | +| C2gPing 1301 / G2cPong 3101 | empty; the client pings every 20 s | +| C2gSubscribe 1309 / C2gUnsubscribe 1308 | queues [1], queues_with_results [2]: (name [1], op_id [2]) | +| G2cJoinedQueue 3106 / G2cLeftQueue 3107 | user_id [1], queue [2], sequence [3]: someone joined or left a queue | +| G2cCreateMount 3108 | queue [1], mount [2], type [3] (1 message, 2 state), properties [4], sequence [5] | +| C2gSendMessage 1304 | op_id [1], queue [2], mount [3], message [4] (bytes) | +| G2cSendMessage 3104 | user_id [1] (bytes), queue [2], mount [3], message [4], sequence [5] | +| C2gStateChange 1305 | op_id [1], queue [2], mount [3], properties [4]: (key [1], value [2]) | +| G2cStateChange 3105 | op_id [1], user_id [2], queue [3], mount [4], properties [5], sequence [6] | +| C2gSuicide 1307 / G2cConnectionClosed 3109 | closing | + +Each queue's messages are numbered (`sequence`), which the client sends back in `OpenFloodgates` to catch up after a +reconnect. + +On `/chat/`: + +- Mount `messages`: JSON `{"userId", "chatId", "message", "to"}` (`to` 0 for everyone). The client takes the sender + from the gateway's `user_id` (the last path segment) and flags a mismatch with `userId` as forged. +- Mount `control`: JSON with a `command`, such as `PlayAction` and `PlayCoopAction` (actions on avatars and + furniture). + +## Chat commands + +Chat text starting with `*` is a command (handled in the client; others see the effect, not the text). Seen or +handled: + +- Presence and seats: `*seat `, `*resume `, `*accept `, `*uid`, `*boot`, `*msg SeatAssignment <...>`. +- Outfit: `*use ` (the whole outfit, also re-sent about every 30 s), `*remove`, `*putOn`, `*putOnOutfit`, + `*takeOff`, `*removeMood`, `*saveOutfit`, `*undo`, `*redo`. +- Typing: `*msg BeginText <...>` before a line of chat. +- Scene: `*imvu:setRoomState ` (from the room's owner; others ignore it from anyone else), + `*imvu:changeRoom`, `*imvu:trigger` / `*imvu:untrigger`, `*imvu:activateMusic` / `*imvu:deactivateMusic`. +- Other: `*imvu:gift`, `*imvu:showGift`, `*imvu:purchase`, `*imvu:try`, `*imvu:goto`, `*imvu:recommend`, + `*imvu:isPureUser`, `*imvu:txnBoot`, `*snap`, `*hiResSnap`, `*uploadSnap`. + +Saving a scene: the owner's client calls `test.setRoomState({room_instance_id, room_contents, ...})`. + +## What the hub must do for a hosted room + +Through the proxy, which sees both channels: + +- **XML-RPC**: answer `getRoomCardInfo`, `getOrMakeChat`, `getRoomState`, `getParticipants`, `terminateChat`, + `bootOutOfChat`, `setRoomState` (owners), and the invitation calls, for hosted rooms' IDs. These go to the hub, + never to IMVU. +- **IMQ**: be the gateway for hosted rooms' queues (`/chat/`, and `room_occupancy/` for counts): results for + subscribe operations, joined and left notices, relaying `SendMessage` to the queue's other members with sequence + numbers, catching up on `OpenFloodgates`, and `control` messages. The proxy splits the one IMQ connection by + queue: hosted rooms' queues to the hub, the rest to IMVU. It answers operations on hub queues itself (keeping the + client's op IDs) and never forwards them. +- **IDs**: hosted rooms need room instance IDs and chat IDs IMVU will never use, so the proxy can tell them apart + (to be chosen: a reserved range, as for products). +- **Room list**: hosted rooms must appear in the client's room list, which comes from a web API (not looked at yet). + +The hub keeps the authoritative participant list, seats, scene and the room's moderation (owner, moderators, bans), +and checks every message's sender itself. + +## Not yet looked at + +- What's sent on `inv:/room/room-`. +- The room list's API, and joining by link (`imvu:room/...`, which calls `getJoinPublicRoomInfo`). +- Moving furniture in one's own room (a capture in My Room). +- Private rooms, invitations and "Chat Now" (one-to-one). +- What `G2cCreateMount` and state mounts are used for in rooms.