Add tournament and multiplayer community chat moderation
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 12m17s

This commit is contained in:
2026-10-27 12:54:15 +05:00
parent b8a430d4c2
commit 9c314ecb4e
19 changed files with 1070 additions and 16 deletions

View File

@@ -93,6 +93,64 @@ export async function multiplayerApi(
409,
);
const action = path[2] ?? "view";
if (
action === "community" &&
path.length === 3 &&
["GET", "POST"].includes(request.method)
) {
if (!savedSecret)
throw new ServiceError("Room membership permission required", 403);
const input =
request.method === "POST" ? await jsonBody(request) : { action: "view" };
const schema = z.discriminatedUnion("action", [
z.object({ action: z.literal("view") }).strict(),
z
.object({
action: z.literal("slow"),
seconds: z.number().int().min(0).max(300),
})
.strict(),
...(["admin", "unadmin", "kick", "ban"] as const).map((action) =>
z
.object({ action: z.literal(action), playerId: z.string().uuid() })
.strict(),
),
z
.object({ action: z.literal("unban"), banId: z.string().uuid() })
.strict(),
z
.object({
action: z.literal("tag"),
playerId: z.string().uuid(),
tag: z
.string()
.trim()
.max(32)
.refine(
(value) =>
!/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(
value,
),
),
})
.strict(),
]);
const body = schema.parse(input);
if (request.method === "POST") {
if (body.action === "view") throw new ServiceError("Not found", 404);
await limit(`multiplayer:moderation:${accessHash}`, 30, 60);
}
return respond(
required(
await client.rpc("party_community", {
p_room: id,
p_access_hash: accessHash,
p_action: body.action,
p_payload: body,
}),
),
);
}
if (action === "discord" && path.length === 3 && request.method === "POST") {
const view = required(
await client.rpc("multiplayer_action", {

View File

@@ -13,7 +13,11 @@ import {
panoramaPath,
viewpointSchema,
} from "@/shared/protocol";
import { roomGuessSchema, roomRoundSchema } from "@/shared/multiplayer";
import {
chatMessageSchema,
roomGuessSchema,
roomRoundSchema,
} from "@/shared/multiplayer";
import { ServiceError, checkServiceError } from "./service-error";
type Dependencies = {
@@ -82,6 +86,75 @@ export async function tournamentApi(
throw new ServiceError("Not found", 404);
const id = z.string().uuid().parse(path[1]);
const action = path[2] ?? "view";
if (["community", "chat"].includes(action)) {
if (!["GET", "POST"].includes(request.method))
throw new ServiceError("Not found", 404);
const account = await deps.account();
if (!account)
throw new ServiceError("Sign in to join or play a tournament", 401);
if (action === "chat") {
const body =
request.method === "POST"
? chatMessageSchema.strict().parse(await deps.jsonBody(request))
: null;
if (body) await deps.limit(`tournaments:chat:${account.id}`, 60, 60);
return respond(
unwrap(
await db.rpc("tournament_chat", {
p_tournament: id,
p_account: account.id,
p_text: body?.text ?? null,
p_client_id: body?.clientId ?? null,
}),
),
);
}
const schema = z.discriminatedUnion("action", [
z
.object({
action: z.literal("slow"),
seconds: z.number().int().min(0).max(300),
})
.strict(),
...(["admin", "unadmin", "kick", "ban", "unban"] as const).map((action) =>
z
.object({ action: z.literal(action), accountId: z.string().uuid() })
.strict(),
),
z
.object({
action: z.literal("tag"),
accountId: z.string().uuid(),
tag: z
.string()
.trim()
.max(32)
.refine(
(value) =>
!/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(
value,
),
),
})
.strict(),
]);
const body =
request.method === "POST"
? schema.parse(await deps.jsonBody(request))
: { action: "view" as const };
if (request.method === "POST")
await deps.limit(`tournaments:moderation:${account.id}`, 30, 60);
return respond(
unwrap(
await db.rpc("tournament_community", {
p_tournament: id,
p_account: account.id,
p_action: body.action,
p_payload: body,
}),
),
);
}
const read =
request.method === "GET" && (action === "view" || action === "media");
const write =