tg-bot-ts/src/systems/registry/ephemeral-registry.ts

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();
},
};