Files
t5de-hub/docs/hosted-rooms.md
T
dhkatzandClaude Opus 5.5 76c8e0342e
CI / test (push) Successful in 1m10s
Hosted rooms phase 2: moderators, bans, privacy, invites, images, favorites, ratings, recent rooms, occupancy watch
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]>
2026-10-30 06:07:41 -07:00

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 is 2100000000 + 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).
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 sends room.seat. maxUsers counts 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. For messages the hub checks the JSON's userId is 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.joined has the list).
  • {"type": "room.closed", "room": n}: the room closed, or this session is out of it: its IMVU account was unlinked or it sent session with another account, or the room became adult and its account isn't. (A lower maxUsers sends 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 (activity publicroom-<owner>-<2100000000+n>: joins, answers chatId, seat, lastMessageId, participantUrl), test.getRoomState (the scene, with community products as local IDs), chat.getParticipants, chat.terminateChat, test.setRoomState (owners: saved with PUT .../scene).
  • IMQ: subscribing to /chat/<chatId> of a hosted room is answered by the proxy (result, JoinedQueue, CreateMounts for control, messages, participants); SendMessage on it goes to the hub as room.send, and room.message comes back as G2cSendMessage. inv:/room/room-<owner>-<2100000000+n> is answered with a result and a node mount, and nothing more.
  • Room list: hosted rooms are added to rooms_list_paginated.php's answer, and room_occupancy/<owner>-<2100000000+n> queues are answered from room.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}. ban true stores a ban; minutes 0 (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": "..."} with reason "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 get room.participants; watchers room.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.boot or hosted_room.ban, target the room number, detail {imvu: X, by: sender's IMVU ID} (and minutes for a ban), actor the sender's person. A banned moderator stays a moderator (the owner removes them from moderators).

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}, or ok: false with reason "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", and message. 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 invite also 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. invite ids 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 sends participants: [] and stops watching it. A session closing stops its watching.

The proxy's part (phase 2)

  • Room cards and lists: privacy, image, score/ratings from the room views; people in a room by name from participants (name), or GET .../participants for a room it isn't in or watching.
  • room_occupancy/<owner>-<2100000000+n> queues: from room.watch/room.occupancy instead of polling.