add date() formatter, add PersistentMessage feature, refactor updates to use PersistentMessage

This commit is contained in:
Nuno Duque Nunes 2026-06-12 23:23:19 +01:00
parent c26d2047a9
commit 63e3a63a7c
4 changed files with 149 additions and 16 deletions

2
.gitignore vendored
View file

@ -20,7 +20,7 @@ data/leaves.json
data/sessionPreferences.json data/sessionPreferences.json
data/tg-history/ data/tg-history/
data/updates/.message-ids.json data/updates/.message-ids.json
data/.message-ids/
# Emoji data # Emoji data
emoji-uploads/ emoji-uploads/

View file

@ -130,6 +130,18 @@ function wrankFull(entry: WRankEntry, options: WRankDisplayOptions): string {
return `${dash} (${dash}${zero})`; // "— ( — 0 )" when others have delta 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 ──────────────────────────────────────────────────────── // ─── Bringer formatters ────────────────────────────────────────────────────────
function bringerDisplay(n: Nation): string { function bringerDisplay(n: Nation): string {
@ -148,6 +160,7 @@ export const format = {
nation, nation,
score, score,
emoji, emoji,
date,
wrank: { wrank: {
rank: wrankRank, rank: wrankRank,
delta: wrankDelta, delta: wrankDelta,

View file

@ -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<string, string> {
return Store.readOrDefault<Record<string, string>>(storePath(store), {});
}
function writeStore(store: MessageStore, data: Record<string, string>): 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<void> {
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));
},
};

View file

@ -23,6 +23,7 @@
import { Nation, VoteEntry, PollState } from "@types"; import { Nation, VoteEntry, PollState } from "@types";
import { WRank } from "@systems/wrank"; import { WRank } from "@systems/wrank";
import { Leaves } from "@systems/leaves"; import { Leaves } from "@systems/leaves";
import { PersistentMessage } from "@systems/persistent-message";
const log = Logger.for("updates"); const log = Logger.for("updates");
@ -96,19 +97,6 @@
return path.join(updatesDir(), ".message-ids.json"); return path.join(updatesDir(), ".message-ids.json");
} }
// ─── Message ID helpers ───────────────────────────────────────────────────────
function getMessageId(version: string): string | null {
const ids = Store.readOrDefault<Record<string, string>>(messageIdsPath(), {});
return ids[version] ?? null;
}
function saveMessageId(version: string, messageId: string): void {
const ids = Store.readOrDefault<Record<string, string>>(messageIdsPath(), {});
ids[version] = messageId;
Store.write(messageIdsPath(), ids);
}
// ─── Versions cache ─────────────────────────────────────────────────────────── // ─── Versions cache ───────────────────────────────────────────────────────────
let _versionsCache: string[] | null = null; let _versionsCache: string[] | null = null;
@ -257,7 +245,7 @@
const channel = await client.channels.fetch(channelId) as TextChannel; const channel = await client.channels.fetch(channelId) as TextChannel;
const embeds = Updates.buildEmbeds(entry); 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()}`); log.debug(`post: version=${version} messageId=${messageId} idsPath=${messageIdsPath()}`);
@ -269,11 +257,12 @@
return; return;
} catch { } catch {
log.warn(`Could not edit ${messageId}, posting new`); log.warn(`Could not edit ${messageId}, posting new`);
PersistentMessage.delete({ store: "updates", key: version });
} }
} }
const msg = await channel.send({ embeds }); 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})`); log.info(`Posted ${version} (${msg.id})`);
}, },