Hosted rooms phase 2: moderators, bans, privacy, invites, images, favorites, ratings, recent rooms, occupancy watch
CI / test (push) Successful in 1m10s
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]>
This commit is contained in:
1 parent
43183ec7ee
commit
76c8e0342e
16 files changed
+3851
-73
No files matched your search
@@ -5,6 +5,7 @@ import { sql } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
customType,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
@@ -356,12 +357,99 @@ export const hostedRoom = pgTable(
|
||||
scene: jsonb("scene").$type<SceneItem[]>().notNull().default([]),
|
||||
sceneHash: text("scene_hash").notNull().default(""),
|
||||
closed: boolean("closed").notNull().default(false),
|
||||
// Phase 2 (docs/hosted-rooms.md): "public" or "private" (unlisted; joined by the owner, moderators and invitees).
|
||||
privacy: text("privacy").notNull().default("public"),
|
||||
// Up to 20 IMVU accounts that may boot and ban (and invite to a private room), with their names as known then.
|
||||
moderators: jsonb("moderators").$type<RoomModerator[]>().notNull().default([]),
|
||||
// The room's image (hosted_room_image), by the sha256 of its bytes; null: none.
|
||||
imageHash: text("image_hash"),
|
||||
createdAt: createdAt(),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
(t) => [index("hosted_room_owner").on(t.ownerId), index("hosted_room_created").on(t.createdAt)],
|
||||
);
|
||||
|
||||
/** A hosted room's moderator: an IMVU account, and its name as known when it was added ("" if unknown). */
|
||||
export type RoomModerator = { imvuId: number; username: string };
|
||||
|
||||
const bytea = customType<{ data: Buffer; driverData: Buffer | Uint8Array }>({
|
||||
dataType: () => "bytea",
|
||||
fromDriver: (v) => Buffer.from(v),
|
||||
});
|
||||
|
||||
// A hosted room's image, as uploaded (PNG or JPEG, checked by its magic bytes, up to 512 KB).
|
||||
export const hostedRoomImage = pgTable("hosted_room_image", {
|
||||
n: integer("n")
|
||||
.primaryKey()
|
||||
.references(() => hostedRoom.n, { onDelete: "cascade" }),
|
||||
contentType: text("content_type").notNull(), // "image/png", "image/jpeg"
|
||||
data: bytea("data").notNull(),
|
||||
hash: text("hash").notNull(), // sha256, hex
|
||||
updatedAt: updatedAt(),
|
||||
});
|
||||
|
||||
// IMVU accounts banned from a hosted room; until null: for good.
|
||||
export const hostedRoomBan = pgTable(
|
||||
"hosted_room_ban",
|
||||
{
|
||||
n: integer("n")
|
||||
.notNull()
|
||||
.references(() => hostedRoom.n, { onDelete: "cascade" }),
|
||||
imvuId: bigint("imvu_id", { mode: "number" }).notNull(),
|
||||
byImvuId: bigint("by_imvu_id", { mode: "number" }).notNull(),
|
||||
until: timestamp("until", { withTimezone: true }),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.n, t.imvuId] })],
|
||||
);
|
||||
|
||||
// A person's favorite hosted rooms.
|
||||
export const hostedRoomFavorite = pgTable(
|
||||
"hosted_room_favorite",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
n: integer("n")
|
||||
.notNull()
|
||||
.references(() => hostedRoom.n, { onDelete: "cascade" }),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.n] })],
|
||||
);
|
||||
|
||||
// A person's rating of a hosted room, 1-5.
|
||||
export const hostedRoomRating = pgTable(
|
||||
"hosted_room_rating",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
n: integer("n")
|
||||
.notNull()
|
||||
.references(() => hostedRoom.n, { onDelete: "cascade" }),
|
||||
score: integer("score").notNull(),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.n] }), index("hosted_room_rating_room").on(t.n)],
|
||||
);
|
||||
|
||||
// A person's latest visit to each hosted room (room.join), with the IMVU account they joined as. 50 are kept.
|
||||
export const hostedRoomVisit = pgTable(
|
||||
"hosted_room_visit",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
n: integer("n")
|
||||
.notNull()
|
||||
.references(() => hostedRoom.n, { onDelete: "cascade" }),
|
||||
imvuId: bigint("imvu_id", { mode: "number" }).notNull(),
|
||||
at: timestamp("at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.n] }), index("hosted_room_visit_at").on(t.userId, t.at)],
|
||||
);
|
||||
|
||||
// ---- Decorations over IMVU rooms (docs/products.md, "Decorations") ----
|
||||
|
||||
/** A decoration: [furniture instance ID, hub product ID, placement], as the client's room_contents. */
|
||||
|
||||
Reference in new issue
Block a user