Add Void difficulty and publish changelogs
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s

This commit is contained in:
2026-10-20 14:05:01 +05:00
parent 3ba398a838
commit de0e891b62
37 changed files with 995 additions and 65 deletions

View File

@@ -188,14 +188,16 @@ export async function handle(request: NextRequest, path: string[]) {
await client
.from("difficulty_presets")
.select(
"id,name,seconds,zoom,pan,compass,coordinates,map_zoom,scoring",
"id,name,seconds,zoom,pan,compass,coordinates,map_zoom,map_pan,timer_visible,scoring",
)
.order("name", { ascending: true }),
);
return NextResponse.json({
presets: presets.map(({ map_zoom, ...preset }) => ({
presets: presets.map(({ map_zoom, map_pan, timer_visible, ...preset }) => ({
...preset,
mapZoom: map_zoom,
mapPan: map_pan,
timerVisible: timer_visible,
})),
});
}
@@ -205,17 +207,17 @@ export async function handle(request: NextRequest, path: string[]) {
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
"unknown";
await limit("difficulty-presets:" + hash(address), 8, 3600);
const { mapZoom, ...fields } = body;
const { mapZoom, mapPan, timerVisible, ...fields } = body;
const { data, error } = await client
.from("difficulty_presets")
.insert({ ...fields, map_zoom: mapZoom })
.insert({ ...fields, map_zoom: mapZoom, map_pan: mapPan, timer_visible: timerVisible })
.select(
"id,name,seconds,zoom,pan,compass,coordinates,map_zoom,scoring",
"id,name,seconds,zoom,pan,compass,coordinates,map_zoom,map_pan,timer_visible,scoring",
)
.single();
checkDifficultyPresetError(error);
return NextResponse.json({
preset: data ? { ...data, mapZoom: data.map_zoom } : null,
preset: data ? { ...data, mapZoom: data.map_zoom, mapPan: data.map_pan, timerVisible: data.timer_visible } : null,
});
}
}
@@ -247,7 +249,7 @@ export async function handle(request: NextRequest, path: string[]) {
const body = z
.object({
datasetId: idSchema,
difficulty: z.union([z.enum(["easy", "normal", "hard"]), idSchema]),
difficulty: z.union([z.enum(["easy", "normal", "hard", "void"]), idSchema]),
})
.parse(await jsonBody(request));
const secret = cookieSecret(request) || randomBytes(32).toString("hex");

76
lib/changelog-feed.ts Normal file
View File

@@ -0,0 +1,76 @@
import {
parseReleasePost,
RELEASE_NEWS_CHANNEL,
RELEASE_WEBHOOK_ID,
type ReleasePost,
} from "../shared/changelog";
export async function readChangelogFeed({
token,
channel = RELEASE_NEWS_CHANNEL,
webhookId = RELEASE_WEBHOOK_ID,
before,
request = fetch,
}: {
token: string;
channel?: string;
webhookId?: string;
before?: string;
request?: typeof fetch;
}) {
if (!/^\d{17,20}$/.test(channel) || (before && !/^\d{17,20}$/.test(before)))
throw new Error("Invalid news channel or cursor");
const url = new URL(
`https://discord.com/api/v10/channels/${channel}/messages`,
);
url.searchParams.set("limit", "100");
if (before) url.searchParams.set("before", before);
// Reading history only. No Discord write endpoint is used by this feature.
const response = await request(url.toString(), {
method: "GET",
headers: { Authorization: `Bot ${token}` },
next: { revalidate: 60 },
signal: AbortSignal.timeout(10_000),
redirect: "error",
});
if (!response.ok)
throw new Error(`News feed unavailable (${response.status})`);
const posts: ReleasePost[] = await response.json();
if (
!Array.isArray(posts) ||
posts.some(
(post) =>
typeof post.content !== "string" ||
post.channel_id !== channel ||
!/^\d{17,20}$/.test(post.id),
)
)
throw new Error("Invalid news response");
if (
posts.some((post) => post.webhook_id === webhookId) &&
posts
.filter((post) => post.webhook_id === webhookId)
.every((post) => !post.content.trim())
)
throw new Error(
"Release text is unavailable; check Discord Message Content access",
);
const entries = posts
.flatMap((post) => {
const entry = parseReleasePost(post, webhookId);
return entry ? [entry] : [];
})
.sort(
(a, b) =>
b.publishedAt.localeCompare(a.publishedAt) ||
(BigInt(a.id) > BigInt(b.id) ? -1 : 1),
);
const oldest = posts.reduce<string | undefined>(
(id, post) => (!id || BigInt(post.id) < BigInt(id) ? post.id : id),
undefined,
);
return {
entries,
before: posts.length === 100 && oldest !== before ? oldest : undefined,
};
}

21
lib/changelog.ts Normal file
View File

@@ -0,0 +1,21 @@
import "server-only";
import { readChangelogFeed } from "./changelog-feed";
export async function getChangelog(before?: string) {
const token = process.env.DISCORD_BOT_TOKEN?.trim();
if (!token) return { entries: [], unavailable: true };
try {
return {
...(await readChangelogFeed({
token,
before,
channel: process.env.DISCORD_NEWS_CHANNEL_ID?.trim() || undefined,
webhookId: process.env.DISCORD_RELEASE_WEBHOOK_ID?.trim() || undefined,
})),
unavailable: false,
};
} catch {
// Do not expose Discord details or credentials to the public page.
return { entries: [], unavailable: true };
}
}

View File

@@ -14,7 +14,7 @@ export function checkDifficultyPresetError(
throw new ServiceError("A difficulty with that name already exists", 409);
if (error?.code === "23514")
throw new ServiceError(
"Use a name with 2–40 characters and a round time of 15–600 seconds in 15-second steps",
"Use a name with 2–40 characters and a round time of 5–600 seconds in 5-second steps",
400,
);
checkServiceError(error);