Add MineGeo gameplay systems and interface

This commit is contained in:
2026-10-10 10:27:47 +05:00
parent bf01d9801d
commit 9af71c85c2
47 changed files with 9954 additions and 93 deletions

View File

@@ -21,7 +21,17 @@ import { moderatorUsersApi } from "./moderator-users-api";
import { contributionCutoff, reportSchema } from "../shared/moderation";
import { siteSettingsSchema } from "../shared/site-settings";
import { getSiteSettings } from "./site-settings";
import {
canReviewMaps,
isAdministrator,
type StaffRole,
} from "@/shared/permissions";
import { processUploadAfterResponse } from "./process-uploads";
import {
discordAfterResponse,
discordStatus,
checkDiscordSetup,
} from "./discord-announcements";
import { needsProcessing } from "./fabric-jobs";
import { archiveMetadataSchema, importManifest } from "../shared/map-archive";
import {
@@ -56,14 +66,19 @@ async function contributor(request: NextRequest) {
if (error) throw new Error(error.message);
return owner;
}
async function moderator(request: NextRequest) {
async function moderator(request: NextRequest, adminOnly = false) {
const owner = await contributor(request);
const profile = required(
await admin().from("profiles").select("role").eq("id", owner).single(),
);
if (profile.role !== "moderator")
throw new Error("Moderator permission required");
return owner;
if (!canReviewMaps(profile.role))
throw new ServiceError("Moderator permission required", 403);
if (adminOnly) requireAdministrator(profile.role);
return { id: owner, role: profile.role };
}
function requireAdministrator(role: StaffRole) {
if (!isAdministrator(role))
throw new ServiceError("Admin permission required", 403);
}
async function limit(key: string, count: number, seconds: number) {
const allowed = required(
@@ -903,10 +918,42 @@ export async function handle(request: NextRequest, path: string[]) {
}
}
if (path[0] === "moderation") {
const reviewer = await moderator(request);
if (path[1] === "users")
const { id: reviewer, role } = await moderator(request);
if (path[1] === "discord" && path.length === 2) {
requireAdministrator(role);
if (method === "GET") return NextResponse.json(await discordStatus());
if (method === "POST") {
const body = z
.object({ action: z.enum(["check", "retry"]) })
.strict()
.parse(await jsonBody(request));
if (body.action === "check")
return NextResponse.json(await checkDiscordSetup());
required(
await client.rpc("retry_discord_announcements", {
p_reviewer: reviewer,
}),
);
required(
await client
.from("discord_announcements")
.update({ cleanup_lease_until: null })
.not("cleanup_error", "is", null)
.is("cleanup_run_id", null)
.is("deleted_at", null)
.select("id"),
);
discordAfterResponse();
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
if (path[1] === "users") {
requireAdministrator(role);
return await moderatorUsersApi(request, path, reviewer, jsonBody);
}
if (path[1] === "settings" && path.length === 2 && method === "PATCH") {
requireAdministrator(role);
const body = siteSettingsSchema.parse(await jsonBody(request));
const enabled = required(
await client.rpc("set_beta_badge", {
@@ -922,6 +969,7 @@ export async function handle(request: NextRequest, path: string[]) {
);
}
if (path[1] === "appeals" && path.length === 3 && method === "POST") {
requireAdministrator(role);
const body = z
.object({
decision: z.enum(["approve", "reject"]),
@@ -946,6 +994,7 @@ export async function handle(request: NextRequest, path: string[]) {
method === "GET" &&
[4, 5].includes(path.length)
) {
requireAdministrator(role);
const report = required(
await client
.from("reports")
@@ -975,7 +1024,11 @@ export async function handle(request: NextRequest, path: string[]) {
path[4],
);
}
if (method === "GET" && path[2] === "panoramas") {
if (
method === "GET" &&
path[2] === "panoramas" &&
[3, 4].includes(path.length)
) {
const d = required(
await client
.from("datasets")
@@ -987,6 +1040,14 @@ export async function handle(request: NextRequest, path: string[]) {
throw new Error("Map is not awaiting review or published");
if (d.parent_dataset_id)
throw new Error("Contributions require owner approval first");
if (
!isAdministrator(role) &&
!(
d.state === "review" ||
(d.state === "published" && d.update_state === "review")
)
)
throw new ServiceError("Map is not awaiting review", 403);
const input = required(
(d.state === "published" && d.update_state !== "review") ||
d.state === "unpublished"
@@ -1007,29 +1068,36 @@ export async function handle(request: NextRequest, path: string[]) {
path[3],
);
}
if (method === "GET") {
const maps = required(
await client
.from("datasets")
.select("*")
.is("parent_dataset_id", null)
.in("state", ["review", "published", "unpublished"])
.order("created_at"),
);
const reports = required(
await client
.from("reports")
.select("*")
.eq("state", "open")
.order("created_at"),
);
const appeals = required(
await client
.from("map_appeals")
.select("*")
.eq("state", "open")
.order("created_at"),
);
if (method === "GET" && path.length === 1) {
let mapQuery = client
.from("datasets")
.select("*")
.is("parent_dataset_id", null)
.in("state", ["review", "published", "unpublished"])
.order("created_at");
if (!isAdministrator(role))
mapQuery = mapQuery.or(
"state.eq.review,and(state.eq.published,update_state.eq.review)",
);
const maps = required(await mapQuery);
const reports = isAdministrator(role)
? required(
await client
.from("reports")
.select("*")
.eq("state", "open")
.order("created_at"),
)
: [];
const appeals = isAdministrator(role)
? required(
await client
.from("map_appeals")
.select("*")
.eq("state", "open")
.order("created_at"),
)
: [];
const reportViews = await Promise.all(
reports.map(async (report) => {
const revision = report.revision_id
@@ -1058,6 +1126,7 @@ export async function handle(request: NextRequest, path: string[]) {
}),
);
return NextResponse.json({
role,
maps: await Promise.all(
maps.map(async (m) => {
const input =
@@ -1087,7 +1156,7 @@ export async function handle(request: NextRequest, path: string[]) {
appeals,
});
}
if (method === "POST") {
if (method === "POST" && path.length === 1) {
const b = z
.object({
datasetId: idSchema,
@@ -1095,6 +1164,7 @@ export async function handle(request: NextRequest, path: string[]) {
reason: z.string().max(2000).optional(),
})
.parse(await jsonBody(request));
if (b.decision === "unpublish") requireAdministrator(role);
if (b.decision === "reject" && !b.reason?.trim())
throw new Error("Provide a rejection reason");
const { error } = await client.rpc("publish_dataset", {
@@ -1104,6 +1174,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_reason: b.reason ?? null,
});
checkServiceError(error);
if (b.decision === "approve") discordAfterResponse();
return NextResponse.json({ ok: true });
}
}
@@ -1164,7 +1235,7 @@ export async function handle(request: NextRequest, path: string[]) {
return NextResponse.json({ id });
}
if (path[0] === "reports" && path.length === 2 && method === "PATCH") {
const reviewer = await moderator(request);
const { id: reviewer } = await moderator(request, true);
const b = z
.object({
decision: z.enum(["confirm", "dismiss"]),