feat(discord) : sync latest Lunaris version and report status
This commit is contained in:
@@ -26,6 +26,7 @@ CATALOG_SYNC_CONCURRENCY=12
|
||||
# Discord-triggered Lunaris catalog sync
|
||||
DISCORD_BOT_TOKEN=replace-with-discord-bot-token
|
||||
DISCORD_CHANNEL_ID=replace-with-private-channel-id
|
||||
DISCORD_LOG_CHANNEL_ID=1547193755772125184
|
||||
|
||||
# S3-compatible private media storage
|
||||
S3_ENDPOINT=https://s3.example.internal
|
||||
|
||||
@@ -122,9 +122,11 @@ source of truth.
|
||||
|
||||
The Discord catalog worker watches the private channel configured by
|
||||
`DISCORD_CHANNEL_ID`. It parses Lunaris `Version Change` announcements and
|
||||
synchronizes that exact version. On startup it checks recent channel history,
|
||||
so it does not need to poll Lunaris while idle. Enable the Discord Message
|
||||
Content intent and grant the bot View Channel and Read Message History.
|
||||
then fetches and synchronizes the latest version reported by Lunaris. On startup
|
||||
it checks recent channel history, so it does not need to poll Lunaris while idle.
|
||||
Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord
|
||||
Message Content intent and grant the bot View Channel, Read Message History, and
|
||||
Send Messages in the configured channels.
|
||||
|
||||
Public snapshots use a shared Redis-backed Next.js cache. Admin routes remain
|
||||
dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so
|
||||
|
||||
+42
-9
@@ -7,7 +7,10 @@ import {
|
||||
} from "discord.js";
|
||||
|
||||
import { closeDb } from "@/db/client";
|
||||
import { processCatalogSync } from "@/lib/catalog/sync";
|
||||
import {
|
||||
getLatestLunarisVersion,
|
||||
processCatalogSync,
|
||||
} from "@/lib/catalog/sync";
|
||||
import {
|
||||
compareLunarisVersions,
|
||||
parseLunarisVersionChange,
|
||||
@@ -54,6 +57,7 @@ function wait(milliseconds: number, signal: AbortSignal): Promise<boolean> {
|
||||
|
||||
const token = required("DISCORD_BOT_TOKEN");
|
||||
const channelId = required("DISCORD_CHANNEL_ID");
|
||||
const logChannelId = required("DISCORD_LOG_CHANNEL_ID");
|
||||
const shutdown = new AbortController();
|
||||
const client = new Client({
|
||||
intents: [
|
||||
@@ -66,40 +70,68 @@ const client = new Client({
|
||||
let pendingVersion: string | null = null;
|
||||
let runner: Promise<void> | null = null;
|
||||
|
||||
async function syncWithRetry(version: string): Promise<void> {
|
||||
async function notify(event: string, details: Record<string, unknown> = {}): Promise<void> {
|
||||
try {
|
||||
const channel = await client.channels.fetch(logChannelId);
|
||||
if (!channel?.isSendable()) {
|
||||
log("notification_failed", { event, error: "Discord log channel is not sendable." });
|
||||
return;
|
||||
}
|
||||
await channel.send(`**Lunaris catalog worker — ${event}**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\``);
|
||||
} catch (cause) {
|
||||
log("notification_failed", {
|
||||
event,
|
||||
error: cause instanceof Error ? cause.message : String(cause),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWithRetry(announcedVersion: string): Promise<void> {
|
||||
for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt += 1) {
|
||||
if (shutdown.signal.aborted) return;
|
||||
if (
|
||||
pendingVersion &&
|
||||
compareLunarisVersions(pendingVersion, version) > 0
|
||||
compareLunarisVersions(pendingVersion, announcedVersion) > 0
|
||||
) {
|
||||
log("sync_superseded", { version, pendingVersion });
|
||||
log("sync_superseded", { announcedVersion, pendingVersion });
|
||||
return;
|
||||
}
|
||||
if (!(await wait(RETRY_DELAYS_MS[attempt], shutdown.signal))) return;
|
||||
if (
|
||||
pendingVersion &&
|
||||
compareLunarisVersions(pendingVersion, version) > 0
|
||||
compareLunarisVersions(pendingVersion, announcedVersion) > 0
|
||||
) {
|
||||
log("sync_superseded", { version, pendingVersion });
|
||||
log("sync_superseded", { announcedVersion, pendingVersion });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log("sync_started", { version, attempt: attempt + 1 });
|
||||
const version = await getLatestLunarisVersion();
|
||||
const details = { announcedVersion, version, attempt: attempt + 1 };
|
||||
log("sync_started", details);
|
||||
await notify("sync started", details);
|
||||
const result = await processCatalogSync(version);
|
||||
log(result === "synced" ? "sync_completed" : "sync_ignored", {
|
||||
announcedVersion,
|
||||
version,
|
||||
reason: result === "unchanged" ? "current-or-older" : undefined,
|
||||
});
|
||||
await notify(result === "synced" ? "sync completed" : "already current", {
|
||||
announcedVersion,
|
||||
version,
|
||||
});
|
||||
return;
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
if (attempt === RETRY_DELAYS_MS.length - 1) {
|
||||
log("sync_failed", { version, attempts: attempt + 1, error: message });
|
||||
const details = { announcedVersion, attempts: attempt + 1, error: message };
|
||||
log("sync_failed", details);
|
||||
await notify("sync failed", details);
|
||||
return;
|
||||
}
|
||||
log("sync_retrying", { version, attempt: attempt + 1, error: message });
|
||||
const details = { announcedVersion, attempt: attempt + 1, error: message };
|
||||
log("sync_retrying", details);
|
||||
await notify("sync retrying", details);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +185,7 @@ try {
|
||||
await Promise.race([ready, stopped]);
|
||||
if (!shutdown.signal.aborted) {
|
||||
log("discord_ready", { userId: client.user?.id, channelId });
|
||||
await notify("worker ready", { userId: client.user?.id, channelId });
|
||||
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
if (
|
||||
|
||||
@@ -48,6 +48,8 @@ spec:
|
||||
configMapKeyRef:
|
||||
name: buzz-sheet-config
|
||||
key: NEXT_DEPLOYMENT_ID
|
||||
- name: DISCORD_LOG_CHANNEL_ID
|
||||
value: "1547193755772125184"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
||||
@@ -155,6 +155,7 @@ describe("environment template contract", () => {
|
||||
"NEXT_DEPLOYMENT_ID",
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_CHANNEL_ID",
|
||||
"DISCORD_LOG_CHANNEL_ID",
|
||||
];
|
||||
|
||||
for (const key of requiredKeys) {
|
||||
|
||||
Reference in New Issue
Block a user