9.8 KiB
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.phpforchat.*,client-dynamic.imvu.com/api/xmlrpc/client.phpfortest.*): 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:
test.getRoomCardInfo({room_instance_id}): the room's card (name, owner, flags likeallowApProducts,allowLoadNewRoom). Room instance IDs look like<owner id>-<n>.chat.getOrMakeChat({userId, activity: "publicroom-<room instance id>", chatId: 0, publicroom: true, private}): returnschatId(an integer),seat,lastMessageId,participantUrl,partnerId. A "declined" response carriesreasonandexplanation(shown as an error). A "waiting" response makes the client wait and retry withinviteId.test.getRoomState({room_instance_id}): the scene:room_hash, androom_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.- IMQ: subscribe to
/chat/<chatId>(with propertylisten_to_self: false). The gateway answers withJoinedQueueand creates three mounts:controlandmessages(message type) andparticipants(state type). Then subscribe toinv:/room/room-<room instance id>, which has onenodemount (message type); nothing arrived on it in a session without room edits. - Chat commands sent on joining (below):
*msg SeatAssignment ...,*seat <n>,*resume <userId>(or*accept <userId>). 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=1for 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, ...), plusnumber_of_rooms,rooms_per_pageand 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 mountoccupantswhose properties are the people in the room, keyed by user ID; the gateway sendsStateChangefrom useradminwhen someone joins (value set) or leaves (value empty). - Joining by link (
imvu:room/...) callstest.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"}(to0 for everyone). The client takes the sender from the gateway'suser_id(the last path segment) and flags a mismatch withuserIdas forged. - Mount
control: JSON with acommand, such asPlayActionandPlayCoopAction(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 letterc, 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_pidis 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:setRoomStatewhen entering the room and after a change, and saves it withtest.setRoomState({room_instance_id, room_contents, ...}); others only accept it from the owner.getRoomStatereturns the same contents with aroom_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>, androom_occupancy/for counts): results for subscribe operations, joined and left notices, relayingSendMessageto the queue's other members with sequence numbers, catching up onOpenFloodgates, andcontrolmessages. 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.phpresponses, answerroom_occupancy/queues for them, and answergetJoinPublicRoomInfofor their links. - Scenes: store the scene from the owner's
setRoomState, serve it fromgetRoomState, and relay the owner's*imvu:setRoomStatelike 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
getOrMakeChatwithprivate: 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, withgetOrMakeChatreturningwaitinguntil 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).