155 lines
4.2 KiB
TypeScript
155 lines
4.2 KiB
TypeScript
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;
|