Hosted rooms' instance IDs name their owner: <owner>-<2100000000+n>
CI / test (push) Successful in 58s

The client takes a room's owner from the instance ID's first part; with 2100000000 there, the owner
couldn't decorate their hosted room (putting furniture on went to their home room instead).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 05:18:19 -07:00
1 parent f65f36c1ea
commit 43183ec7ee
7 files changed
+17 -13

No files matched your search

+1 -1
View File
@@ -227,7 +227,7 @@ export function createApp({ db, auth, imvu, realtime: rt, age: ageService, rooms
const hostedView = ({ room: r, ownerName }: { room: typeof schema.hostedRoom.$inferSelect; ownerName: string }) => ({
n: r.n,
instanceId: instanceId(r.n),
instanceId: instanceId(r.ownerImvuId, r.n),
chatId: chatId(r.n),
name: r.name,
description: r.description,
+2 -1
View File
@@ -32,7 +32,8 @@ export const decorationHash = (contents: DecorationItem[]) =>
function ownerOf(instance: string): number {
if (!decorationInstance.test(instance)) throw new DecorationError(400, "invalid", "That isn't an IMVU room.");
const owner = Number(instance.slice(0, instance.indexOf("-")));
if (owner >= hostedBase) throw new DecorationError(400, "hosted_room", "Hosted rooms keep their own scene.");
if (Number(instance.slice(instance.indexOf("-") + 1)) >= hostedBase)
throw new DecorationError(400, "hosted_room", "Hosted rooms keep their own scene.");
return owner;
}
+3 -1
View File
@@ -31,7 +31,9 @@ export const maxPlacement = 200;
export const sceneHash = (roomPid: number, contents: SceneItem[]) =>
createHash("sha256").update(JSON.stringify([roomPid, contents])).digest("hex");
export const instanceId = (n: number) => `${hostedBase}-${n}`;
// "<owner's IMVU ID>-<hostedBase + n>": the client takes a room's owner from the first part (so the owner can
// decorate it), and IMVU's own room numbers are far below hostedBase.
export const instanceId = (owner: number, n: number) => `${owner}-${hostedBase + n}`;
export const chatId = (n: number) => hostedBase + n;
// eslint-disable-next-line no-control-regex
+2 -2
View File
@@ -119,7 +119,7 @@ test("only the room's owner decorates it; contents are checked", async () => {
const put = (contents: unknown, instance = room) => call(owner, "PUT", `/api/v1/decorations/${instance}`, { contents });
for (const bad of ["502", "0502-3", "502-0", "502-3-4", "x-1", "1234567890123-1"])
expect((await put([item], bad)).status).toBe(400);
expect((await put([item], "2100000000-5")).json.error).toBe("hosted_room");
expect((await put([item], "12345-2100000005")).json.error).toBe("hosted_room");
expect((await put([[7, "AAAAAAAAAA", ""]])).status).toBe(400);
expect((await put([[7, 12345, ""]])).status).toBe(400);
expect((await put([[-1, "aaaaaaaaaa", ""]])).status).toBe(400);
@@ -157,7 +157,7 @@ test("viewers see the products they may, with a hash of what they see", async ()
// Unknown rooms have nothing.
expect((await call(undefined, "GET", "/api/v1/decorations/601-9")).json).toEqual({ instance: "601-9", contents: [], hash: hash([]) });
expect((await call(undefined, "GET", "/api/v1/decorations/601-x")).status).toBe(400);
expect((await call(undefined, "GET", "/api/v1/decorations/2100000000-1")).status).toBe(400);
expect((await call(undefined, "GET", "/api/v1/decorations/12345-2100000001")).status).toBe(400);
const put = await call(owner, "PUT", `/api/v1/decorations/${room}`, { contents: [g, a, r] });
expect(put.status).toBe(200);
+1 -1
View File
@@ -133,7 +133,7 @@ test("rooms are made, listed, changed and closed by their owners", async () => {
expect(made.status).toBe(201);
const n = made.json.room.n;
expect(made.json.room).toMatchObject({
instanceId: `2100000000-${n}`,
instanceId: `302-${2_100_000_000 + n}`,
chatId: 2_100_000_000 + n,
name: "The Lounge",
description: "A quiet place.",
+2 -2
View File
@@ -65,12 +65,12 @@ the `GET`s also signed out.
| `DELETE /api/v1/hosted-rooms/:n` | (owner or moderator) closes it for good; everyone in it gets `room.closed` |
A room is `{n, instanceId, chatId, name, description, roomPid, rating, maxUsers, ownerImvuId, ownerName, occupancy,
createdAt}`; `instanceId` is `2100000000-<n>`, `chatId` 2100000000 + n, `occupancy` the people in it now.
createdAt}`; `instanceId` is `<owner IMVU ID>-<2100000000 + n>`, `chatId` 2100000000 + n, `occupancy` the people in it now.
## Decorations
T5 products placed in ordinary IMVU rooms (docs/products.md, "Decorations"). `instance` is IMVU's room instance,
`<owner's customer ID>-<n>` (`^[1-9][0-9]{0,11}-[1-9][0-9]{0,9}$`); hosted rooms' (`2100000000-<n>`) are refused
`<owner's customer ID>-<n>` (`^[1-9][0-9]{0,11}-[1-9][0-9]{0,9}$`); hosted rooms' (room number 2100000000 and up) are refused
(400 `hosted_room`: they have their own scene). A room's decorations are `{instance, contents, hash}`: `contents`
is a list of `[furniture instance ID (integer), hub product ID, placement (up to 200 characters)]`, `hash` the
sha256 (hex) of `contents` as JSON.
+6 -5
View File
@@ -12,8 +12,9 @@ owner's furniture), occupancy in the room list. Later: moderators, bans and boot
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 `2100000000-<n>` and its **chat ID** is
`2100000000 + n`. (IMVU's chat IDs were about 1,390,000,000 in 2026-10, and XML-RPC integers end at
- 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).
@@ -71,12 +72,12 @@ 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-2100000000-<n>`: joins, answers `chatId`, `seat`, `lastMessageId`,
`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-2100000000-<n>` is answered with a result and a
`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/2100000000-<n>`
- **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`.