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

154 lines
9.8 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`). The gateway answers with
`JoinedQueue` and creates three mounts: `control` and `messages` (message type) and `participants` (state type).
Then subscribe to `inv:/room/room-<room instance id>`, which has one `node` mount (message type); nothing arrived
on it in a session without room edits.
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
- Rooms come from `client-dynamic.imvu.com/api/rooms/rooms_list_paginated.php` (signed in; `recent=1` for recently
visited, search parameters otherwise). The response is column-oriented JSON: one array per field
(`customers_room_id`, `name`, `customers_name`, `num_participants`, `max_users`, `participants`, `image_url`,
`description`, `rating`, `language`, `is_ap`, `is_av`, `is_vip`, ...), plus `number_of_rooms`, `rooms_per_page`
and welcome-room fields. The proxy can add hosted rooms by appending to every array.
- For live counts, the list subscribes to `room_occupancy/<room instance id>` for each room shown (the client
re-subscribes them as the list refreshes). Each is a state mount `occupants` whose properties are the people in
the room, keyed by user ID; the gateway sends `StateChange` from user `admin` when someone joins (value set) or
leaves (value empty).
- Joining by link (`imvu:room/...`) calls `test.getJoinPublicRoomInfo({publicroom_url})`, which returns the room
instance ID.
## 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>`:
- The gateway sends a sender's own messages back to them too (with the queue's next sequence number); the client
ignores its own because it subscribed with `listen_to_self: false`.
- 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`.
## The scene
- Encoding (`*imvu:setRoomState`): the letter `c`, then base64 (with line breaks) of zlib-compressed JSON:
`{"room_state": {"room_info": {"customers_id", "room_instance_id", "room_pid", "revision_id"}, "room_contents":
[[furniture instance ID, product ID, placement], ...]}}`. `room_pid` is the room shell's product. Placement is a
string of numbers: a format number, then position, a parent reference (which slot of which furniture item it sits
on), rotation and scale (the exact layout still to be worked out).
- The owner's client broadcasts the **whole scene** with `*imvu:setRoomState` when entering the room and after a
change, and saves it with `test.setRoomState({room_instance_id, room_contents, ...})`; others only accept it from
the owner. `getRoomState` returns the same contents with a `room_hash`.
- A hosted room's scene can carry community products the same way: they're product IDs like any other (the proxy's
local IDs on each machine, global IDs on the wire).
## 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**: add hosted rooms to `rooms_list_paginated.php` responses, answer `room_occupancy/` queues for them,
and answer `getJoinPublicRoomInfo` for their links.
- **Scenes**: store the scene from the owner's `setRoomState`, serve it from `getRoomState`, and relay the owner's
`*imvu:setRoomState` like any message.
The hub keeps the authoritative participant list, seats, scene and the room's moderation (owner, moderators, bans),
and checks every message's sender itself.
## Other sessions (from the code, not driven)
- **Private rooms**: the same `getOrMakeChat` with `private: true`.
- **Chat Now** (random one-to-one) uses activity `Chat now-<room instance id>`. It pairs strangers, so it wasn't
driven with the test account.
- **Invitations**: `chat.attemptInvite`, `acceptInvite`, `declineInvite`, `leaveInviteAwayNote`, with
`getOrMakeChat` returning `waiting` until the other person answers. Needs a second account to capture.
## Not yet looked at
- Messages on `inv:/room/room-<id>` (likely room edits made outside the client).
- The placement string's exact layout.
- The furniture panel: dragging furniture in (a browser page; synthesized mouse drags don't trigger it).