Add MineGeo gameplay systems and interface
This commit is contained in:
154
lib/discord-cleanup-workflow.ts
Normal file
154
lib/discord-cleanup-workflow.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
FatalError,
|
||||
RetryableError,
|
||||
getWorkflowMetadata,
|
||||
sleep,
|
||||
} from "workflow";
|
||||
import { admin, required } from "./supabase";
|
||||
import {
|
||||
DiscordClient,
|
||||
DiscordDeliveryError,
|
||||
discordConfiguration,
|
||||
} from "./discord-client";
|
||||
|
||||
export async function cleanupClosedDiscordParty(event: string, claim: string) {
|
||||
"use workflow";
|
||||
const run = getWorkflowMetadata().workflowRunId;
|
||||
try {
|
||||
const deadline = await markClosed(event);
|
||||
if (deadline) {
|
||||
await sleep(new Date(deadline));
|
||||
await deleteClosed(event);
|
||||
}
|
||||
} catch {
|
||||
await releaseFailedCleanup(event, claim, run);
|
||||
throw new Error(
|
||||
"Discord party cleanup failed; the saved announcement can be retried.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function record(event: string) {
|
||||
const result = await admin()
|
||||
.from("discord_announcements")
|
||||
.select("*")
|
||||
.eq("id", event)
|
||||
.maybeSingle();
|
||||
if (result.error) required(result);
|
||||
const job = result.data;
|
||||
return job?.kind === "party" &&
|
||||
job.state === "sent" &&
|
||||
job.closed_at &&
|
||||
!job.deleted_at &&
|
||||
job.channel_id &&
|
||||
job.message_id
|
||||
? job
|
||||
: null;
|
||||
}
|
||||
|
||||
function client() {
|
||||
const config = discordConfiguration(process.env);
|
||||
if (!config.token || !config.origin)
|
||||
throw new FatalError(
|
||||
"Configure the Discord bot token and public site URL.",
|
||||
);
|
||||
return { client: new DiscordClient(config.token), origin: config.origin! };
|
||||
}
|
||||
|
||||
function retry(error: unknown): never {
|
||||
if (error instanceof DiscordDeliveryError) {
|
||||
if (error.permanent) throw new FatalError(error.message);
|
||||
throw new RetryableError(error.message, {
|
||||
retryAfter: error.retryAfter * 1000,
|
||||
});
|
||||
}
|
||||
throw new RetryableError("Discord cleanup could not be confirmed.", {
|
||||
retryAfter: "30s",
|
||||
});
|
||||
}
|
||||
|
||||
async function markClosed(event: string): Promise<string | null> {
|
||||
"use step";
|
||||
try {
|
||||
const job = await record(event);
|
||||
if (!job) return null;
|
||||
if (job.delete_after) return job.delete_after;
|
||||
const setup = client();
|
||||
const exists = await setup.client.closeParty(
|
||||
job.channel_id!,
|
||||
job.message_id!,
|
||||
setup.origin,
|
||||
job.target_id,
|
||||
);
|
||||
if (!exists) {
|
||||
required(
|
||||
await admin()
|
||||
.from("discord_announcements")
|
||||
.update({ deleted_at: new Date().toISOString(), cleanup_error: null })
|
||||
.eq("id", event)
|
||||
.select("id")
|
||||
.single(),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const result = await admin().rpc("mark_discord_party_closed", {
|
||||
p_event: event,
|
||||
});
|
||||
if (result.error) required(result);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (error instanceof FatalError || error instanceof RetryableError)
|
||||
throw error;
|
||||
retry(error);
|
||||
}
|
||||
}
|
||||
markClosed.maxRetries = 10;
|
||||
|
||||
async function deleteClosed(event: string) {
|
||||
"use step";
|
||||
try {
|
||||
const job = await record(event);
|
||||
if (!job) return;
|
||||
if (!job.delete_after)
|
||||
throw new FatalError("The closed notice has not been confirmed.");
|
||||
const deadline = new Date(job.delete_after);
|
||||
if (deadline.getTime() > Date.now())
|
||||
throw new RetryableError("Keep the five-minute grace period.", {
|
||||
retryAfter: deadline,
|
||||
});
|
||||
await client().client.remove(job.channel_id!, job.message_id!);
|
||||
required(
|
||||
await admin()
|
||||
.from("discord_announcements")
|
||||
.update({ deleted_at: new Date().toISOString(), cleanup_error: null })
|
||||
.eq("id", event)
|
||||
.select("id")
|
||||
.single(),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof FatalError || error instanceof RetryableError)
|
||||
throw error;
|
||||
retry(error);
|
||||
}
|
||||
}
|
||||
deleteClosed.maxRetries = 10;
|
||||
|
||||
async function releaseFailedCleanup(event: string, claim: string, run: string) {
|
||||
"use step";
|
||||
required(
|
||||
await admin()
|
||||
.from("discord_announcements")
|
||||
.update({
|
||||
cleanup_run_id: null,
|
||||
cleanup_lease_until: new Date(Date.now() + 300000).toISOString(),
|
||||
cleanup_error:
|
||||
"Party cleanup failed. Check Discord setup and retry saved announcements.",
|
||||
})
|
||||
.eq("id", event)
|
||||
.in("cleanup_run_id", [claim, run])
|
||||
.is("deleted_at", null)
|
||||
.select("id"),
|
||||
);
|
||||
console.error("Discord party cleanup failed", { event, run });
|
||||
}
|
||||
releaseFailedCleanup.maxRetries = 10;
|
||||
Reference in New Issue
Block a user