Files
t5de-hub/apps/web/app/rooms/actions.ts
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

107 lines
3.7 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { api, unwrap } from "@/lib/api";
import { attempt, field, type ActionState } from "@/lib/actions";
const rating = (form: FormData) => (field(form, "rating") === "adult" ? ("adult" as const) : ("general" as const));
const privacy = (form: FormData) => (field(form, "privacy") === "private" ? ("private" as const) : ("public" as const));
const moderators = (form: FormData) =>
field(form, "moderators")
.split(/[,\n]/)
.map((s) => s.trim())
.filter(Boolean);
/** Makes a hosted room, then goes back to the list. */
export async function createRoom(_: ActionState, form: FormData): Promise<ActionState> {
const state = await attempt(async () => {
unwrap(
await api["hosted-rooms"].post({
name: field(form, "name"),
description: field(form, "description"),
roomPid: Number(field(form, "roomPid")),
rating: rating(form),
maxUsers: Number(field(form, "maxUsers")),
imvuId: Number(field(form, "imvuId")),
privacy: privacy(form),
}),
);
revalidatePath("/rooms");
});
if (state?.error) return state;
redirect("/rooms");
}
/** The owner's changes to a room. */
export async function saveRoom(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
const state = await attempt(async () => {
unwrap(
await api["hosted-rooms"]({ n }).patch({
name: field(form, "name"),
description: field(form, "description"),
roomPid: Number(field(form, "roomPid")),
rating: rating(form),
maxUsers: Number(field(form, "maxUsers")),
privacy: privacy(form),
moderators: moderators(form),
}),
);
revalidatePath("/rooms");
});
if (state?.error) return state;
redirect("/rooms");
}
/** The room's image: a PNG or JPEG, up to 512 KB. */
export async function saveImage(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
const image = form.get("image");
if (!(image instanceof File) || image.size === 0) return { error: "Choose an image." };
if (image.size > 512 * 1024) return { error: "An image is up to 512 KB." };
return attempt(async () => {
unwrap(await api["hosted-rooms"]({ n }).image.put({ image }));
revalidatePath("/rooms");
revalidatePath(`/rooms/${n}/edit`);
return "Image saved.";
});
}
export async function removeImage(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
return attempt(async () => {
unwrap(await api["hosted-rooms"]({ n }).image.delete());
revalidatePath("/rooms");
revalidatePath(`/rooms/${n}/edit`);
});
}
/** Stars a room, or (favorite=0) unstars it. */
export async function setFavorite(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
return attempt(async () => {
const room = api["hosted-rooms"]({ n }).favorite;
unwrap(await (field(form, "favorite") === "1" ? room.put() : room.delete()));
revalidatePath("/rooms");
});
}
export async function rateRoom(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
return attempt(async () => {
unwrap(await api["hosted-rooms"]({ n }).rating.put({ score: Number(field(form, "score")) }));
revalidatePath("/rooms");
});
}
/** Closes a room: everyone in it is sent out. */
export async function closeRoom(_: ActionState, form: FormData): Promise<ActionState> {
const n = field(form, "n");
const state = await attempt(async () => {
unwrap(await api["hosted-rooms"]({ n }).delete());
revalidatePath("/rooms");
});
if (state?.error) return state;
redirect("/rooms");
}