From 43183ec7ee5e2a80bf8d0c83fa1593550b52898b Mon Sep 17 00:00:00 2001 From: dhkatz Date: Fri, 30 Oct 2026 05:18:19 -0700 Subject: [PATCH] Hosted rooms' instance IDs name their owner: -<2100000000+n> 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 --- apps/api/src/app.ts | 2 +- apps/api/src/decorations.ts | 3 ++- apps/api/src/hosted.ts | 4 +++- apps/api/test/decorations.test.ts | 4 ++-- apps/api/test/hosted.test.ts | 2 +- docs/api.md | 4 ++-- docs/hosted-rooms.md | 11 ++++++----- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index d34b4bc..40a40cc 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -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, diff --git a/apps/api/src/decorations.ts b/apps/api/src/decorations.ts index 5803ef1..703e552 100644 --- a/apps/api/src/decorations.ts +++ b/apps/api/src/decorations.ts @@ -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; } diff --git a/apps/api/src/hosted.ts b/apps/api/src/hosted.ts index 92a0961..cfdd3e7 100644 --- a/apps/api/src/hosted.ts +++ b/apps/api/src/hosted.ts @@ -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}`; +// "-": 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 diff --git a/apps/api/test/decorations.test.ts b/apps/api/test/decorations.test.ts index 639829c..c7e802f 100644 --- a/apps/api/test/decorations.test.ts +++ b/apps/api/test/decorations.test.ts @@ -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); diff --git a/apps/api/test/hosted.test.ts b/apps/api/test/hosted.test.ts index a81c0b7..6a00ac0 100644 --- a/apps/api/test/hosted.test.ts +++ b/apps/api/test/hosted.test.ts @@ -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.", diff --git a/docs/api.md b/docs/api.md index 9cfa5ff..831f49e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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-`, `chatId` 2100000000 + n, `occupancy` the people in it now. +createdAt}`; `instanceId` is `-<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, -`-` (`^[1-9][0-9]{0,11}-[1-9][0-9]{0,9}$`); hosted rooms' (`2100000000-`) are refused +`-` (`^[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. diff --git a/docs/hosted-rooms.md b/docs/hosted-rooms.md index ed908da..faeadc7 100644 --- a/docs/hosted-rooms.md +++ b/docs/hosted-rooms.md @@ -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-` 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 `-<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-`: joins, answers `chatId`, `seat`, `lastMessageId`, + `chat.getOrMakeChat` (activity `publicroom--<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/` 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-` is answered with a result and a + `room.message` comes back as `G2cSendMessage`. `inv:/room/room--<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-` +- **Room list**: hosted rooms are added to `rooms_list_paginated.php`'s answer, and `room_occupancy/-<2100000000+n>` queues are answered from `room.participants`.