From 63e3a63a7c8eed648150cb3c5ccc51f5ec07fc47 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 12 Jun 2026 23:23:19 +0100 Subject: [PATCH] add date() formatter, add PersistentMessage feature, refactor updates to use PersistentMessage --- .gitignore | 2 +- src/systems/format.ts | 13 +++ src/systems/persistent-message.ts | 131 ++++++++++++++++++++++++++++++ src/systems/updates.ts | 19 +---- 4 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 src/systems/persistent-message.ts diff --git a/.gitignore b/.gitignore index 84dd42e..fc1d38d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ data/leaves.json data/sessionPreferences.json data/tg-history/ data/updates/.message-ids.json - +data/.message-ids/ # Emoji data emoji-uploads/ diff --git a/src/systems/format.ts b/src/systems/format.ts index 42fa7fd..d839952 100644 --- a/src/systems/format.ts +++ b/src/systems/format.ts @@ -130,6 +130,18 @@ function wrankFull(entry: WRankEntry, options: WRankDisplayOptions): string { return `${dash} (${dash}${zero})`; // "— ( — 0 )" when others have delta } +// ─── Date formatters ──────────────────────────────────────────────────────── + +function date(date: Date | string, fmt: string = "dd/MM/YYYY"): string { + const d = typeof date === "string" ? new Date(date) : date; + return fmt + .replace("dd", String(d.getDate()).padStart(2, "0")) + .replace("MM", String(d.getMonth() + 1).padStart(2, "0")) + .replace("YYYY", String(d.getFullYear())) + .replace("HH", String(d.getHours()).padStart(2, "0")) + .replace("mm", String(d.getMinutes()).padStart(2, "0")); +}, + // ─── Bringer formatters ──────────────────────────────────────────────────────── function bringerDisplay(n: Nation): string { @@ -148,6 +160,7 @@ export const format = { nation, score, emoji, + date, wrank: { rank: wrankRank, delta: wrankDelta, diff --git a/src/systems/persistent-message.ts b/src/systems/persistent-message.ts new file mode 100644 index 0000000..2831e3f --- /dev/null +++ b/src/systems/persistent-message.ts @@ -0,0 +1,131 @@ +/** + * PersistentMessage — manages Discord messages that need to be edited in place. + * Each store maps to a separate file in data/.message-ids/ + * + * Usage: + * import { PersistentMessage } from "@systems/persistent-message"; + * + * await PersistentMessage.post({ + * store: "leaderboard", + * key: "2026-W24", + * channelId: "123456", + * embeds, + * client, + * }); + * + * PersistentMessage.get({ store: "leaderboard", key: "2026-W24" }) // messageId | null + */ + + import path from "path"; + import { Client, EmbedBuilder, TextChannel } from "discord.js"; + import { Store } from "@systems/store"; + import { Paths } from "@paths"; + import { Logger } from "@systems/logger"; + + const log = Logger.for("persistent-message"); + + // ─── Types ──────────────────────────────────────────────────────────────────── + + export type MessageStore = "updates" | "leaderboard" | "results"; + + interface PostParams { + store: MessageStore; + key: string; + channelId: string; + embeds: EmbedBuilder[]; + client: Client; + } + + interface GetParams { + store: MessageStore; + key: string; + } + + interface SetParams { + store: MessageStore; + key: string; + messageId: string; + } + + interface DeleteParams { + store: MessageStore; + key: string; + } + + // ─── Helpers ────────────────────────────────────────────────────────────────── + + function storePath(store: MessageStore): string { + return Paths.data(".message-ids", `${store}.json`); + } + + function readStore(store: MessageStore): Record { + return Store.readOrDefault>(storePath(store), {}); + } + + function writeStore(store: MessageStore, data: Record): void { + Store.write(storePath(store), data); + } + + // ─── Namespace ──────────────────────────────────────────────────────────────── + + export const PersistentMessage = { + /** + * Get the stored messageId for a key in a store. + */ + get({ store, key }: GetParams): string | null { + return readStore(store)[key] ?? null; + }, + + /** + * Store a messageId for a key. + */ + set({ store, key, messageId }: SetParams): void { + const data = readStore(store); + data[key] = messageId; + writeStore(store, data); + }, + + /** + * Delete a stored messageId. + */ + delete({ store, key }: DeleteParams): void { + const data = readStore(store); + delete data[key]; + writeStore(store, data); + }, + + /** + * Post or edit a persistent message. + * If a messageId exists for the key, edits the existing message. + * If not, posts a new message and stores the messageId. + */ + async post({ store, key, channelId, embeds, client }: PostParams): Promise { + const channel = await client.channels.fetch(channelId) as TextChannel; + const messageId = PersistentMessage.get({ store, key }); + + log.debug(`post: store=${store} key=${key} messageId=${messageId}`); + + if (messageId) { + try { + const msg = await channel.messages.fetch(messageId); + await msg.edit({ embeds }); + log.info(`Edited ${store}/${key} (${messageId})`); + return; + } catch (err: any) { + log.warn(`Could not edit ${messageId}, posting new: ${err.message}`); + PersistentMessage.delete({ store, key }); + } + } + + const msg = await channel.send({ embeds }); + PersistentMessage.set({ store, key, messageId: msg.id }); + log.info(`Posted ${store}/${key} (${msg.id})`); + }, + + /** + * List all keys in a store. + */ + list({ store }: { store: MessageStore }): string[] { + return Object.keys(readStore(store)); + }, + }; \ No newline at end of file diff --git a/src/systems/updates.ts b/src/systems/updates.ts index bc3688f..0e2836d 100644 --- a/src/systems/updates.ts +++ b/src/systems/updates.ts @@ -23,6 +23,7 @@ import { Nation, VoteEntry, PollState } from "@types"; import { WRank } from "@systems/wrank"; import { Leaves } from "@systems/leaves"; + import { PersistentMessage } from "@systems/persistent-message"; const log = Logger.for("updates"); @@ -96,19 +97,6 @@ return path.join(updatesDir(), ".message-ids.json"); } - // ─── Message ID helpers ─────────────────────────────────────────────────────── - - function getMessageId(version: string): string | null { - const ids = Store.readOrDefault>(messageIdsPath(), {}); - return ids[version] ?? null; - } - - function saveMessageId(version: string, messageId: string): void { - const ids = Store.readOrDefault>(messageIdsPath(), {}); - ids[version] = messageId; - Store.write(messageIdsPath(), ids); - } - // ─── Versions cache ─────────────────────────────────────────────────────────── let _versionsCache: string[] | null = null; @@ -257,7 +245,7 @@ const channel = await client.channels.fetch(channelId) as TextChannel; const embeds = Updates.buildEmbeds(entry); - const messageId = getMessageId(version); + const messageId = PersistentMessage.get({ store: "updates", key: version }); log.debug(`post: version=${version} messageId=${messageId} idsPath=${messageIdsPath()}`); @@ -269,11 +257,12 @@ return; } catch { log.warn(`Could not edit ${messageId}, posting new`); + PersistentMessage.delete({ store: "updates", key: version }); } } const msg = await channel.send({ embeds }); - saveMessageId(version, msg.id); + PersistentMessage.set({ store: "updates", key: version, messageId: msg.id }); log.info(`Posted ${version} (${msg.id})`); },