Add MineGeo gameplay systems and interface
This commit is contained in:
135
lib/api.ts
135
lib/api.ts
@@ -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"]),
|
||||
|
||||
Reference in New Issue
Block a user