CI / test (push) Successful in 1m10s
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]>
198 lines
15 KiB
Markdown
198 lines
15 KiB
Markdown
# 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).
|
|
|
|
## 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 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`,
|
|
`CreateMount`s 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.
|