Files
t5de-hub/docs/imq-rooms.md
T
2026-10-29 02:10:42 -07:00

119 lines
7.1 KiB
Markdown

# 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 `<owner id>-<n>`.
2. `chat.getOrMakeChat({userId, activity: "publicroom-<room instance id>", 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/<chatId>` (with property `listen_to_self: false`), then `inv:/room/room-<room instance
id>` (contents not seen yet).
5. Chat commands sent on joining (below): `*msg SeatAssignment ...`, `*seat <n>`, `*resume <userId>` (or
`*accept <userId>`).
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/<room instance id>` 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/<chatId>`:
- 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 <n>`, `*resume <id>`, `*accept <id>`, `*uid`, `*boot`, `*msg SeatAssignment <...>`.
- Outfit: `*use <ids...>` (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 <encoded scene>` (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/<chatId>`, 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-<id>`.
- 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.