Moderators (up to 20 IMVU accounts, set by ID or name) and the owner account boot and ban over realtime (room.boot); bans are listed and lifted over REST. Private rooms are unlisted and joined by the owner, moderators and holders of an accepted invite (room.invite / room.invited / room.invite_reply / room.invite_answer). Room images (PNG/JPEG by magic bytes, up to 512 KB) are stored in the database and served with cache headers. Per-person favorites, ratings and recent rooms. room.watch pushes room.occupancy; participants carry their IMVU name, and GET .../participants lists them. Contract in docs/hosted-rooms.md, "Phase 2". Co-Authored-By: Claude Opus 5.5 <[email protected]>
15 KiB
T5DE-hosted rooms (phase 1)
A hosted room lives on the hub (design.md, Rooms): T5DE users find it in the client's room list, join it, sit and chat, and its owner decorates it. The client treats it as an ordinary IMVU room: T5DE's proxy answers the client's room calls and runs the room's IMQ queue against the hub (imq-rooms.md has what the client expects). IMVU never hears of hosted rooms.
Phase 1: rooms created on the website; joining, seats, chat (messages and control mounts), the scene (the
owner's furniture), occupancy in the room list. Phase 2 (below): moderators, boots and bans, private rooms,
invitations, images, favorites, ratings, recent rooms, occupancy pushed to watchers, participant names.
IDs
IDs IMVU won't use, so the proxy can tell hosted rooms apart:
- A room has a number
n(1, 2, ...), its instance ID is<owner's IMVU ID>-<2100000000 + n>and its chat ID is2100000000 + n. The client takes a room's owner from the instance ID's first part (so the owner can decorate it); IMVU's own room numbers are small. (IMVU's chat IDs were about 1,390,000,000 in 2026-10, and XML-RPC integers end at 2,147,483,647; IMVU customer IDs, the first part of its instance IDs, are far below 2,100,000,000.) - The room shell is an IMVU product (
roomPid), chosen when the room is made (any room product; the client loads it like any room).
REST (bearer or cookie session)
| Route | |
|---|---|
POST /api/v1/hosted-rooms |
{name, description, roomPid, rating, maxUsers, imvuId}: make a room. imvuId is one of the person's linked IMVU accounts, shown as the owner. rating general or adult (adult needs an adult person). maxUsers 2-30, default 10. Name 1-64 characters, description up to 512. A person may own up to 10 open rooms. |
GET /api/v1/hosted-rooms |
(also signed out) {rooms: [room]}, open rooms, newest first; adult rooms only for adults. ?mine=1: the person's own. |
GET /api/v1/hosted-rooms/:n |
(also signed out) {room, scene: {contents, hash}}; 404 not_found (or adult, for others), 410 closed. |
PATCH /api/v1/hosted-rooms/:n |
owner: {name?, description?, rating?, maxUsers?, roomPid?}. |
PUT /api/v1/hosted-rooms/:n/scene |
owner: {contents, roomPid?}, from the client's test.setRoomState. |
DELETE /api/v1/hosted-rooms/:n |
owner (or moderator console): closes it; everyone in it is told (room.closed). |
A room: {n, instanceId, chatId, name, description, roomPid, rating, maxUsers, ownerImvuId, ownerName, occupancy, createdAt} (ownerName: the owner's IMVU name as linked; occupancy: people in it now). A room whose owner account
is no longer linked to its owner (unlinked, or moved to another person), or whose owner is banned, is hidden (not
listed, not_found) until it's linked again. Closed rooms stay closed; their numbers aren't reused.
The scene's contents is the client's room_contents: a list of [furniture instance ID (integer), product ID (integer or a hub product ID string), placement (string)], up to 500 items, placement up to 200 characters. hash
is a hash of the stored scene (the client caches scenes by it): sha256, hex, of [roomPid, contents] as JSON.
Realtime (the proxy's WebSocket)
From the proxy (the session's IMVU account, sent with session, must be linked to the person):
{"type": "room.join", "room": n}: join. Answer{"type": "room.joined", "room": n, "chat": chatId, "participants": [{"imvu_id", "seat"}], "sequence": s}(the room's latest message number), or{"type": "room.error", "room": n, "reason": "full" | "closed" | "adult" | "not_found" | "not_linked" | "too_many", "message": "..."}(too_many: the session is in 5 hosted rooms already). Joining again (a reconnect) answers the same. A newcomer's seat is 0 until it sendsroom.seat.maxUserscounts IMVU accounts, so the same account joining from a second session isn't refused as full.{"type": "room.leave", "room": n}.{"type": "room.seat", "room": n, "seat": k}: the person sat down (*seat k); 0-99.{"type": "room.send", "room": n, "mount": "messages" | "control", "message": "..."}(up to 64 KB): to everyone else in the room. Formessagesthe hub checks the JSON'suserIdis the sender's IMVU account and drops it otherwise (silently). Each session may send bursts of 20, then 10 a second; more is dropped. Malformed messages (bad room, seat, mount, a message over 64 KB, sending to a room it isn't in) get{"type": "error", "message"}.
From the hub:
{"type": "room.message", "room": n, "from": imvuId, "mount": "...", "message": "...", "sequence": s}: numbered per room, from 1.{"type": "room.participants", "room": n, "participants": [{"imvu_id", "seat"}]}: whenever someone joins, leaves or sits (not to the one joining:room.joinedhas the list).{"type": "room.closed", "room": n}: the room closed, or this session is out of it: its IMVU account was unlinked or it sentsessionwith another account, or the room became adult and its account isn't. (A lowermaxUserssends no one out; it only stops newcomers.)
A session closing leaves its rooms. The hub keeps the participants and message numbers in memory; messages aren't stored (design.md: chat retention).
The proxy's part
- XML-RPC for hosted IDs, answered by the proxy (never sent to IMVU):
test.getRoomCardInfo,chat.getOrMakeChat(activitypublicroom-<owner>-<2100000000+n>: joins, answerschatId,seat,lastMessageId,participantUrl),test.getRoomState(the scene, with community products as local IDs),chat.getParticipants,chat.terminateChat,test.setRoomState(owners: saved withPUT .../scene). - IMQ: subscribing to
/chat/<chatId>of a hosted room is answered by the proxy (result,JoinedQueue,CreateMounts forcontrol,messages,participants);SendMessageon it goes to the hub asroom.send, androom.messagecomes back asG2cSendMessage.inv:/room/room-<owner>-<2100000000+n>is answered with a result and anodemount, and nothing more. - Room list: hosted rooms are added to
rooms_list_paginated.php's answer, androom_occupancy/<owner>-<2100000000+n>queues are answered fromroom.participants.
Phase 2
Moderators, boots and bans, private rooms, invitations, images, favorites, ratings, recent rooms, occupancy pushed to watchers, and participants' names. Everything in phase 1 still holds; this adds to it.
Rooms (REST)
A room (in every answer that has one) also has:
| Field | |
|---|---|
privacy |
"public" (default) or "private" |
moderators |
IMVU account IDs, up to 20, in the order the owner gave them (never the owner's own account) |
moderatorNames |
their IMVU names, same order: the name of the linked account, or as IMVU gave it when added by name; "" when not known |
image |
<API URL>/api/v1/hosted-rooms/<n>/image?v=<16 hex> (absolute; v changes with the image), or null |
favorite |
whether it's one of the signed-in person's favorites (false signed out) |
score |
the average rating, 1-5, two decimals; 0 if nobody rated it. (Named score, not rating: rating is already the room's general/adult.) |
ratings |
how many people rated it |
Who's who: the room's owner is, over REST, the person who made it; over realtime, its owner IMVU account
(ownerImvuId). A moderator is an IMVU account in moderators; over REST, a person is a moderator if one of their
linked accounts is.
| Route | |
|---|---|
POST /api/v1/hosted-rooms |
also takes privacy? |
PATCH /api/v1/hosted-rooms/:n |
(owner) also takes privacy? and moderators?: up to 20 entries, each an IMVU ID (integer, or a string of digits) or an IMVU name (looked up: linked accounts first, then IMVU, rate-limited; unknown: 404 no_such_imvu_account). Replaces the list; duplicates and the owner's account are dropped. More than 20: 400 invalid. Making a room private sends no one out. |
GET /api/v1/hosted-rooms |
public rooms, and private ones the person owns or moderates. ?mine=1: the person's own (private too). ?favorites=1: their favorites, latest starred first. ?recent=1: the last 20 distinct rooms they joined (any of their accounts), latest first. Those three need a signed-in person (401 signed_out); adult rooms only for adults, as before; closed and hidden rooms left out. |
GET /api/v1/hosted-rooms/:n |
anyone may get a private room (it's unlisted, not secret). Also myRating: the person's own rating, 0 if none or signed out. |
GET /api/v1/hosted-rooms/:n/participants |
(anyone who may GET /:n, also signed out; same 404/410) {participants: [{imvu_id, seat, name}]}, as in room.participants |
GET /api/v1/hosted-rooms/:n/bans |
(owner or moderator) {bans: [{imvuId, byImvuId, until, createdAt}]}: bans in force, latest first; until ISO time or null (for good). Others: 403 forbidden. |
DELETE /api/v1/hosted-rooms/:n/bans/:imvu |
(owner only) lifts a ban: 204; 404 if there's none. Logged (hosted_room.unban). |
PUT /api/v1/hosted-rooms/:n/image |
(owner) multipart, field image: a PNG or JPEG (by its magic bytes, whatever the declared type), up to 512 KB; else 400 invalid. Stored as uploaded. → {room}. 30 an hour. |
DELETE /api/v1/hosted-rooms/:n/image |
(owner) → {room} with image: null |
GET /api/v1/hosted-rooms/:n/image |
(anyone who may GET /:n) the image, with its Content-Type, ETag (If-None-Match answers 304), Cache-Control: public, max-age=3600 (private for adult rooms), X-Content-Type-Options: nosniff; 404 if none |
PUT /api/v1/hosted-rooms/:n/favorite |
(signed in, a room they may see) stars it: 204, also if starred already |
DELETE /api/v1/hosted-rooms/:n/favorite |
204 |
PUT /api/v1/hosted-rooms/:n/rating |
{score} (integer 1-5) → {room, myRating}; replaces their earlier rating. Owners can't rate their own room (403 forbidden). |
DELETE /api/v1/hosted-rooms/:n/rating |
removes their rating: 204 |
A visit is recorded when an IMVU account joins a hosted room (a new member; not a re-join of a session already in it): per person, the room, the account and the time; the latest 50 per person are kept.
Realtime
Participants everywhere (room.joined, room.participants, room.occupancy, GET .../participants) are
{"imvu_id": X, "seat": k, "name": "..."}; name is the IMVU username of the hub's verified link for that account
(as of when it joined), "" if unknown.
Joining (room.join) may carry "invite": id. New room.error reasons, checked in this order after phase 1's
not_linked, not_found, closed, adult: "banned" (a ban in force on the session's account), then "private"
(a private room, and the account isn't its owner account, a moderator, already let in, or holding an accepted,
unexpired invite for this room and account), then too_many, full.
An invite used to join is spent, and the account is let in: it may join again without one (a reconnect, another
session) until it sends room.leave and none of its sessions is still in the room, or it's booted. (A connection
dropping doesn't end it. It's kept in memory: a hub restart ends it.)
Boot and ban, from the owner account or a moderator (needn't be in the room):
- proxy → hub
{"type": "room.boot", "room": n, "imvu_id": X, "ban"?: false, "minutes"?: 0}.bantrue stores a ban;minutes0 (default) is for good, else 1 to 5,270,400 (about ten years). Malformed:{"type": "error", "message": "bad imvu_id" | "bad ban" | "bad minutes" | "bad room"}. - hub → proxy (the sender)
{"type": "room.boot_result", "room": n, "imvu_id": X, "ok": true, "ban": bool}, or{"type": "room.boot_result", "room": n, "imvu_id": X, "ok": false, "reason": "...", "message": "..."}withreason"not_linked","not_found","closed"or"forbidden": the sender isn't the owner account or a moderator; X is the owner account (nobody boots the owner); X is a moderator and the sender isn't the owner account; or X is the sender. - Every session of X in the room gets
{"type": "room.closed", "room": n, "reason": "booted"}and is out; the others getroom.participants; watchersroom.occupancy. A boot without a ban doesn't stop X coming back (except into a private room, which needs a new invite). - Logged:
hosted_room.bootorhosted_room.ban, target the room number, detail{imvu: X, by: sender's IMVU ID}(andminutesfor a ban), actor the sender's person. A banned moderator stays a moderator (the owner removes them frommoderators).
Invitations:
- proxy → hub
{"type": "room.invite", "room": n, "imvu_id": X}: from a session in the room (public rooms), or from the owner account or a moderator, in it or not (private rooms). 10, then 10 a minute per session. - hub → proxy (the sender)
{"type": "room.invite_result", "room": n, "imvu_id": X, "ok": true, "invite": id}, orok: falsewithreason"not_linked","not_found","closed","forbidden"(private room, not owner or moderator),"not_in_room"(public room, the sender isn't in it),"banned"(X is banned from the room),"adult"(an adult room, and none of X's sessions counts as adult) or"rate_limited", andmessage. X being the sender:{"type": "error", "message": "bad imvu_id"}. - When
ok: every session whose IMVU account is X gets{"type": "room.invited", "room": n, "invite": id, "from": sender's IMVU ID, "room_name": "...", "owner_name": "..."}(owner_name: the owner account's IMVU name). If X has no session, the sender instead gets{"type": "room.invite_answer", "invite": id, "to": X, "accept": false, "offline": true}right after the result, and the invite is dropped. - proxy (X's) → hub
{"type": "room.invite_reply", "invite": id, "accept": bool, "note"?: "..."}. The first answer counts.note: control characters become spaces, trimmed, up to 200 characters, left out when empty. Errors:{"type": "error", "message": "no such invite" | "already answered" | "bad accept" | "bad note"}(no such invitealso when it expired or the session's account isn't X). - hub → proxy (every session of the sender's IMVU account)
{"type": "room.invite_answer", "invite": id, "to": X, "accept": bool, "note"?: "..."}. - An invite: single use, valid 10 minutes from when it was made (accepting doesn't extend it), for that room and
account. Kept in memory only; closing the room drops its invites.
inviteids are opaque strings (base64url).
Occupancy:
- proxy → hub
{"type": "room.watch", "rooms": [n, ...]}: replaces the session's watched set (up to 100;[]stops). Not a list of room numbers, or over 100:{"type": "error", "message": "bad rooms"}. Rooms the session may not see are left out silently: no such room, closed or hidden; adult rooms for a session whose account isn't adult; private rooms unless its account is the owner account or a moderator. That's checked when it watches. - hub → proxy, once per watched room right after, then whenever the room's participants change (join, leave, seat,
boot):
{"type": "room.occupancy", "room": n, "participants": [{"imvu_id", "seat", "name"}]}. A room closing sendsparticipants: []and stops watching it. A session closing stops its watching.
The proxy's part (phase 2)
- Room cards and lists:
privacy,image,score/ratingsfrom the room views; people in a room by name fromparticipants(name), orGET .../participantsfor a room it isn't in or watching. room_occupancy/<owner>-<2100000000+n>queues: fromroom.watch/room.occupancyinstead of polling.