112 lines
No EOL
3.4 KiB
TypeScript
112 lines
No EOL
3.4 KiB
TypeScript
/**
|
|
* EphemeralRegistry — tracks ephemeral messages so they can be updated later
|
|
* using the original interaction token (works within Discord's 15 minute window).
|
|
*
|
|
* Usage:
|
|
* import { Ephemeral } from "@systems/ephemeral-registry";
|
|
*
|
|
* // Store after sending followUp
|
|
* const msg = await interaction.followUp({ ..., fetchReply: true });
|
|
* Ephemeral.store(voteId, "companion", interaction, msg.id);
|
|
*
|
|
* // Update later (within 15 minutes)
|
|
* await Ephemeral.update(voteId, "companion", "✅ Done", []);
|
|
*
|
|
* // Clear all for a user (e.g. on poll start)
|
|
* Ephemeral.clear(voteId);
|
|
*/
|
|
|
|
import { ButtonInteraction, ChatInputCommandInteraction, Message } from "discord.js";
|
|
|
|
type AnyInteraction = ChatInputCommandInteraction | ButtonInteraction;
|
|
|
|
interface EphemeralEntry {
|
|
interaction: AnyInteraction;
|
|
messageId: string;
|
|
storedAt: number;
|
|
}
|
|
|
|
const DISCORD_TOKEN_TTL_MS = 14 * 60 * 1000; // 14 min (Discord allows 15)
|
|
const registry = new Map<string, EphemeralEntry>();
|
|
|
|
function makeKey(id: string, tag: string): string {
|
|
return `${id}:${tag}`;
|
|
}
|
|
|
|
function isExpired(entry: EphemeralEntry): boolean {
|
|
return Date.now() - entry.storedAt > DISCORD_TOKEN_TTL_MS;
|
|
}
|
|
|
|
export const Ephemeral = {
|
|
/**
|
|
* Store a reference to an ephemeral followUp message.
|
|
* @param id — voter ID or userKey
|
|
* @param tag — identifier (e.g. "companion", "conflict", "score_prompt")
|
|
* @param interaction — the original interaction that sent the followUp
|
|
* @param messageId — the message ID returned from fetchReply: true
|
|
*/
|
|
store(id: string, tag: string, interaction: AnyInteraction, messageId: string): void {
|
|
registry.set(makeKey(id, tag), { interaction, messageId, storedAt: Date.now() });
|
|
},
|
|
|
|
/**
|
|
* Get a stored entry (returns null if not found or expired).
|
|
*/
|
|
get(id: string, tag: string): EphemeralEntry | null {
|
|
const entry = registry.get(makeKey(id, tag));
|
|
if (!entry) return null;
|
|
if (isExpired(entry)) {
|
|
registry.delete(makeKey(id, tag));
|
|
return null;
|
|
}
|
|
return entry;
|
|
},
|
|
|
|
/**
|
|
* Update a stored ephemeral message via the original interaction token.
|
|
* Silently fails if not found, expired, or Discord rejects the edit.
|
|
*/
|
|
async update(
|
|
id: string,
|
|
tag: string,
|
|
content: string,
|
|
components: any[] = [],
|
|
options: { final?: boolean } = { final: true }
|
|
): Promise<void> {
|
|
const entry = Ephemeral.get(id, tag);
|
|
console.log(`[Ephemeral.update] id=${id} tag=${tag} found=${!!entry}`);
|
|
if (!entry) return;
|
|
try {
|
|
console.log(`[Ephemeral.update] editing messageId=${entry.messageId}`);
|
|
await entry.interaction.webhook.editMessage(entry.messageId, { content, components });
|
|
if (options.final !== false) Ephemeral.delete(id, tag);
|
|
console.log(`[Ephemeral.update] success`);
|
|
} catch (err: any) {
|
|
console.error(`[Ephemeral.update] failed:`, err.message);
|
|
Ephemeral.delete(id, tag);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete a stored entry.
|
|
*/
|
|
delete(id: string, tag: string): void {
|
|
registry.delete(makeKey(id, tag));
|
|
},
|
|
|
|
/**
|
|
* Clear all ephemerals for a given ID.
|
|
*/
|
|
clear(id: string): void {
|
|
for (const key of registry.keys()) {
|
|
if (key.startsWith(`${id}:`)) registry.delete(key);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear all entries (e.g. on full poll reset).
|
|
*/
|
|
clearAll(): void {
|
|
registry.clear();
|
|
},
|
|
}; |