From 666986afb179839aff0e373821cf977e59615e94 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 12 Jun 2026 16:27:36 +0100 Subject: [PATCH] refactor update messageId system / Discord.Interaction refactors --- data/updates/v0.1/update.json | 45 ++++++++++++++++------ data/updates/v0.2/update.json | 55 ++++++++++++++++++++------- data/updates/v0.3/update.json | 45 ++++++++++++++++------ data/updates/v0.4/update.json | 40 +++++++++++++++----- data/updates/v0.5/update.json | 50 ++++++++++++++++++------ data/updates/v0.6/update.json | 50 ++++++++++++++++++------ data/updates/v0.7/update.json | 65 ++++++++++++++++++++++++-------- data/updates/v0.8/update.json | 15 ++++---- src/discord/interaction.ts | 32 ++++++++++++++++ src/subcommands/admin/updates.ts | 46 +++++++++++----------- src/systems/updates.ts | 13 +++++-- 11 files changed, 336 insertions(+), 120 deletions(-) diff --git a/data/updates/v0.1/update.json b/data/updates/v0.1/update.json index a0502b5..75daf49 100644 --- a/data/updates/v0.1/update.json +++ b/data/updates/v0.1/update.json @@ -1,25 +1,48 @@ { "version": "v0.1", - "date": "2026-05-01", + "date": "2026-05-28", "title": "Core Poll System", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "Poll creation with `/tg poll start` — opens voting for the upcoming TG", "emojiKey":null }, - { "text": "Poll scheduling with cronjobs — opens voting for the upcoming TGs at specific times", "emojiKey":null }, - { "text": "Yes/No voting with character display — class emoji, level and name", "emojiKey": "wi" }, - { "text": "Nation-separated fields — Capella and Procyon listed independently", "emojiKey": "capella" }, - { "text": "Public messages per player — leave a note when voting", "emojiKey": null }, - { "text": "Poll lock and confirm system — lock at TG start, confirm yes/no after", "emojiKey": null }, - { "text": "W.Rank display — rank number with gold variant for 7 TGs done", "emojiKey": "wrank_1_gold" }, - { "text": "Bringer display — Storm Bringer and Luminous Bringer indicators", "emojiKey": "storm_bringer" } + { + "text": "Poll creation with `/tg poll start` — opens voting for the upcoming TG", + "emojiKey": null + }, + { + "text": "Poll scheduling with cronjobs — opens voting for the upcoming TGs at specific times", + "emojiKey": null + }, + { + "text": "Yes/No voting with character display — class emoji, level and name", + "emojiKey": "wi" + }, + { + "text": "Nation-separated fields — Capella and Procyon listed independently", + "emojiKey": "capella" + }, + { + "text": "Public messages per player — leave a note when voting", + "emojiKey": null + }, + { + "text": "Poll lock and confirm system — lock at TG start, confirm yes/no after", + "emojiKey": null + }, + { + "text": "W.Rank display — rank number with gold variant for 7 TGs done", + "emojiKey": "wrank_1_gold" + }, + { + "text": "Bringer display — Storm Bringer and Luminous Bringer indicators", + "emojiKey": "storm_bringer" + } ] } ], "examples": [] -} +} \ No newline at end of file diff --git a/data/updates/v0.2/update.json b/data/updates/v0.2/update.json index 08a1b4b..f5e2c54 100644 --- a/data/updates/v0.2/update.json +++ b/data/updates/v0.2/update.json @@ -1,22 +1,42 @@ { "version": "v0.2", - "date": "2026-05-10", + "date": "2026-05-30", "title": "Character System & Impersonation", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "Character management — `/tg char add/remove/set-active/set-nation`", "emojiKey": "active_char" }, - { "text": "Character sharing — lend your character to another player with `/tg char share`", "emojiKey": "borrowed" }, - { "text": "Character borrowing — request to play someone else's character", "emojiKey": "borrowed" }, - { "text": "Session borrows and persistent preferences across restarts", "emojiKey": "active_char" }, - { "text": "`/tg switch` — change your active character at any time", "emojiKey": "active_char" }, - { "text": "Impersonation system — officers can vote on behalf of players", "emojiKey": null }, - { "text": "Autocomplete for character names with nation emoji and shared indicator 🔗", "emojiKey": null } + { + "text": "Character management — `/tg char add/remove/set-active/set-nation`", + "emojiKey": "active_char" + }, + { + "text": "Character sharing — lend your character to another player with `/tg char share`", + "emojiKey": "borrowed" + }, + { + "text": "Character borrowing — request to play someone else's character", + "emojiKey": "borrowed" + }, + { + "text": "Session borrows and persistent preferences across restarts", + "emojiKey": "active_char" + }, + { + "text": "`/tg switch` — change your active character at any time", + "emojiKey": "active_char" + }, + { + "text": "Impersonation system — officers can vote on behalf of players", + "emojiKey": null + }, + { + "text": "Autocomplete for character names with nation emoji and shared indicator 🔗", + "emojiKey": null + } ] }, { @@ -24,11 +44,20 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "Character data stored per user in `characters.json`", "emojiKey": null }, - { "text": "Borrow requests tracked with expiry timestamps", "emojiKey": null }, - { "text": "ID-first usermap lookup — survives Discord username changes", "emojiKey": null } + { + "text": "Character data stored per user in `characters.json`", + "emojiKey": null + }, + { + "text": "Borrow requests tracked with expiry timestamps", + "emojiKey": null + }, + { + "text": "ID-first usermap lookup — survives Discord username changes", + "emojiKey": null + } ] } ], "examples": [] -} +} \ No newline at end of file diff --git a/data/updates/v0.3/update.json b/data/updates/v0.3/update.json index 90c1aa2..1f8fd94 100644 --- a/data/updates/v0.3/update.json +++ b/data/updates/v0.3/update.json @@ -1,19 +1,30 @@ { "version": "v0.3", - "date": "2026-05-18", + "date": "2026-06-01", "title": "Conflict Resolution", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "Character conflict detection — warns when two players want the same character", "emojiKey": null }, - { "text": "Conflict embed with Reclaim and Switch buttons", "emojiKey": null }, - { "text": "Reclaim notifies the borrower via DM with a character selection prompt", "emojiKey": null }, - { "text": "Auto-vote on conflict switch — voting Yes automatically after switching", "emojiKey": "active_char" } + { + "text": "Character conflict detection — warns when two players want the same character", + "emojiKey": null + }, + { + "text": "Conflict embed with Reclaim and Switch buttons", + "emojiKey": null + }, + { + "text": "Reclaim notifies the borrower via DM with a character selection prompt", + "emojiKey": null + }, + { + "text": "Auto-vote on conflict switch — voting Yes automatically after switching", + "emojiKey": "active_char" + } ] }, { @@ -21,8 +32,14 @@ "label": "Bug Fixes", "emoji": "🔧", "items": [ - { "text": "Score overwrite fixed for shared characters", "emojiKey": null }, - { "text": "Bringer display corrected after weekly reset", "emojiKey": null } + { + "text": "Score overwrite fixed for shared characters", + "emojiKey": null + }, + { + "text": "Bringer display corrected after weekly reset", + "emojiKey": null + } ] }, { @@ -30,10 +47,16 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "Conflict resolution state machine with proper cleanup", "emojiKey": null }, - { "text": "Reclaim flow uses interaction tokens for ephemeral editing", "emojiKey": null } + { + "text": "Conflict resolution state machine with proper cleanup", + "emojiKey": null + }, + { + "text": "Reclaim flow uses interaction tokens for ephemeral editing", + "emojiKey": null + } ] } ], "examples": [] -} +} \ No newline at end of file diff --git a/data/updates/v0.4/update.json b/data/updates/v0.4/update.json index f0fc4b5..9e69640 100644 --- a/data/updates/v0.4/update.json +++ b/data/updates/v0.4/update.json @@ -1,18 +1,26 @@ { "version": "v0.4", - "date": "2026-05-25", + "date": "2026-06-03", "title": "Companion System", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "Companion ephemeral — after voting Yes, shows your active character with switch buttons", "emojiKey": "active_char" }, - { "text": "Character switch buttons update the poll embed in real time", "emojiKey": "active_char" }, - { "text": "Switching to a taken character triggers the conflict resolution flow", "emojiKey": null } + { + "text": "Companion ephemeral — after voting Yes, shows your active character with switch buttons", + "emojiKey": "active_char" + }, + { + "text": "Character switch buttons update the poll embed in real time", + "emojiKey": "active_char" + }, + { + "text": "Switching to a taken character triggers the conflict resolution flow", + "emojiKey": null + } ] }, { @@ -20,8 +28,14 @@ "label": "Improvements", "emoji": "⚡", "items": [ - { "text": "Interaction lock — prevents double-click issues on all buttons", "emojiKey": null }, - { "text": "Companion ephemeral updates in place instead of spawning a new message", "emojiKey": null } + { + "text": "Interaction lock — prevents double-click issues on all buttons", + "emojiKey": null + }, + { + "text": "Companion ephemeral updates in place instead of spawning a new message", + "emojiKey": null + } ] }, { @@ -29,10 +43,16 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "`EphemeralRegistry` — edit ephemeral messages via interaction token within 15 minute window", "emojiKey": null }, - { "text": "`InteractionLock` — prevents duplicate interaction processing", "emojiKey": null } + { + "text": "`EphemeralRegistry` — edit ephemeral messages via interaction token within 15 minute window", + "emojiKey": null + }, + { + "text": "`InteractionLock` — prevents duplicate interaction processing", + "emojiKey": null + } ] } ], "examples": [] -} +} \ No newline at end of file diff --git a/data/updates/v0.5/update.json b/data/updates/v0.5/update.json index a8d8794..f4c36e9 100644 --- a/data/updates/v0.5/update.json +++ b/data/updates/v0.5/update.json @@ -1,19 +1,30 @@ { "version": "v0.5", - "date": "2026-06-01", + "date": "2026-06-05", "title": "W.Rank Improvements", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "W.Rank delta system — tracks rank movement with <:wrank_up:1512114414474756132><:wrank_down:1511906547104616643> and grey placeholder for unchanged", "emojiKey": "wrank_up" }, - { "text": "Midnight snapshot — after 24 hours of no rank change, delta resets to ( - 0 )", "emojiKey": "wrank_1" }, - { "text": "Weekly reset carries Bringer forward — W.Rank 1 with goal TGs becomes next week's Bringer", "emojiKey": "storm_bringer" }, - { "text": "No-rank placeholder alignment — players without W.Rank align with those who have it", "emojiKey": "wrank_no_dash" } + { + "text": "W.Rank delta system — tracks rank movement with <:wrank_up:1512114414474756132><:wrank_down:1511906547104616643> and grey placeholder for unchanged", + "emojiKey": "wrank_up" + }, + { + "text": "Midnight snapshot — after 24 hours of no rank change, delta resets to ( - 0 )", + "emojiKey": "wrank_1" + }, + { + "text": "Weekly reset carries Bringer forward — W.Rank 1 with goal TGs becomes next week's Bringer", + "emojiKey": "storm_bringer" + }, + { + "text": "No-rank placeholder alignment — players without W.Rank align with those who have it", + "emojiKey": "wrank_no_dash" + } ] }, { @@ -21,8 +32,14 @@ "label": "Improvements", "emoji": "⚡", "items": [ - { "text": "W.Rank now tracked per character, not per player — borrowing a character preserves its rank", "emojiKey": "wrank_2_gold" }, - { "text": "Bringer validation — must be W.Rank 1 AND have 7 TGs, otherwise no Bringer this week", "emojiKey": "luminous_bringer" } + { + "text": "W.Rank now tracked per character, not per player — borrowing a character preserves its rank", + "emojiKey": "wrank_2_gold" + }, + { + "text": "Bringer validation — must be W.Rank 1 AND have 7 TGs, otherwise no Bringer this week", + "emojiKey": "luminous_bringer" + } ] }, { @@ -30,9 +47,18 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "`lastRankChangeAt` timestamp on W.Rank entries — drives the 24h snapshot window", "emojiKey": null }, - { "text": "W.Rank keys migrated from lowercase `capella/procyon` to `Nation` enum values", "emojiKey": null }, - { "text": "`WRankEntry` hydration — runtime entries carry full `Character` object, not just flat fields", "emojiKey": null } + { + "text": "`lastRankChangeAt` timestamp on W.Rank entries — drives the 24h snapshot window", + "emojiKey": null + }, + { + "text": "W.Rank keys migrated from lowercase `capella/procyon` to `Nation` enum values", + "emojiKey": null + }, + { + "text": "`WRankEntry` hydration — runtime entries carry full `Character` object, not just flat fields", + "emojiKey": null + } ] } ], @@ -44,4 +70,4 @@ "file": "examples/poll-wrank.json" } ] -} +} \ No newline at end of file diff --git a/data/updates/v0.6/update.json b/data/updates/v0.6/update.json index 011372d..93001d0 100644 --- a/data/updates/v0.6/update.json +++ b/data/updates/v0.6/update.json @@ -1,19 +1,30 @@ { "version": "v0.6", - "date": "2026-06-05", + "date": "2026-06-07", "title": "Administration & Score Submission", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "`/tg-admin user map/unmap/list` — register Discord accounts to player profiles", "emojiKey": null }, - { "text": "`/tg-admin poll fix-voter` — correct stale poll entries after a restart", "emojiKey": null }, - { "text": "Score submission via Submit Score button after TG ends", "emojiKey": null }, - { "text": "`/tg score get` — retrieve your score for a specific TG slot", "emojiKey": null } + { + "text": "`/tg-admin user map/unmap/list` — register Discord accounts to player profiles", + "emojiKey": null + }, + { + "text": "`/tg-admin poll fix-voter` — correct stale poll entries after a restart", + "emojiKey": null + }, + { + "text": "Score submission via Submit Score button after TG ends", + "emojiKey": null + }, + { + "text": "`/tg score get` — retrieve your score for a specific TG slot", + "emojiKey": null + } ] }, { @@ -21,8 +32,14 @@ "label": "Improvements", "emoji": "⚡", "items": [ - { "text": "ID-first usermap lookup — account links survive Discord username changes", "emojiKey": null }, - { "text": "`UserRegistry` — centralized user identity with caching", "emojiKey": null } + { + "text": "ID-first usermap lookup — account links survive Discord username changes", + "emojiKey": null + }, + { + "text": "`UserRegistry` — centralized user identity with caching", + "emojiKey": null + } ] }, { @@ -30,11 +47,20 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "`CharacterRegistry` — cached character lookups across all users", "emojiKey": null }, - { "text": "`Attendance` system — snapshots who attended each TG at lock time", "emojiKey": null }, - { "text": "`Score` namespace — centralized score submission and retrieval", "emojiKey": null } + { + "text": "`CharacterRegistry` — cached character lookups across all users", + "emojiKey": null + }, + { + "text": "`Attendance` system — snapshots who attended each TG at lock time", + "emojiKey": null + }, + { + "text": "`Score` namespace — centralized score submission and retrieval", + "emojiKey": null + } ] } ], "examples": [] -} +} \ No newline at end of file diff --git a/data/updates/v0.7/update.json b/data/updates/v0.7/update.json index b0822b8..ce83483 100644 --- a/data/updates/v0.7/update.json +++ b/data/updates/v0.7/update.json @@ -1,19 +1,30 @@ { "version": "v0.7", - "date": "2026-06-08", + "date": "2026-06-09", "title": "UI Layout System", "layout": "default", - "messageId": null, "sections": [ { "type": "new", "label": "New Features", "emoji": "✨", "items": [ - { "text": "Poll layout system — multiple display styles, switchable via `/tg-config poll set-layout`", "emojiKey": null }, - { "text": "`default` layout — standard vertical nation fields", "emojiKey": null }, - { "text": "`side-by-side` layout — nations displayed inline, auto-stacks when >5 players per nation", "emojiKey": null }, - { "text": "Layout persists across bot restarts", "emojiKey": null } + { + "text": "Poll layout system — multiple display styles, switchable via `/tg-config poll set-layout`", + "emojiKey": null + }, + { + "text": "`default` layout — standard vertical nation fields", + "emojiKey": null + }, + { + "text": "`side-by-side` layout — nations displayed inline, auto-stacks when >5 players per nation", + "emojiKey": null + }, + { + "text": "Layout persists across bot restarts", + "emojiKey": null + } ] }, { @@ -21,9 +32,18 @@ "label": "Improvements", "emoji": "⚡", "items": [ - { "text": "Voting Yes and No now run in parallel — faster poll embed updates", "emojiKey": null }, - { "text": "Autocomplete filtered by nation for Bringer set command", "emojiKey": null }, - { "text": "Character class now carries full name and emoji — `Force Blader`, `Wizard`, etc.", "emojiKey": null } + { + "text": "Voting Yes and No now run in parallel — faster poll embed updates", + "emojiKey": null + }, + { + "text": "Autocomplete filtered by nation for Bringer set command", + "emojiKey": null + }, + { + "text": "Character class now carries full name and emoji — `Force Blader`, `Wizard`, etc.", + "emojiKey": null + } ] }, { @@ -31,11 +51,26 @@ "label": "Under the Hood", "emoji": "🔩", "items": [ - { "text": "`BaseLayout` — shared functions inherited by all layouts, override only what differs", "emojiKey": null }, - { "text": "Layout auto-discovery — drop a file in `layouts/` and it registers automatically", "emojiKey": null }, - { "text": "`Character` type now carries `ownerKey` and full `CharacterClass` object", "emojiKey": null }, - { "text": "`SerializableCharacter` — clean JSON serialization boundary, runtime uses rich types", "emojiKey": null }, - { "text": "`Nation` converted to string enum — serializes cleanly, exhaustiveness checked by TypeScript", "emojiKey": null } + { + "text": "`BaseLayout` — shared functions inherited by all layouts, override only what differs", + "emojiKey": null + }, + { + "text": "Layout auto-discovery — drop a file in `layouts/` and it registers automatically", + "emojiKey": null + }, + { + "text": "`Character` type now carries `ownerKey` and full `CharacterClass` object", + "emojiKey": null + }, + { + "text": "`SerializableCharacter` — clean JSON serialization boundary, runtime uses rich types", + "emojiKey": null + }, + { + "text": "`Nation` converted to string enum — serializes cleanly, exhaustiveness checked by TypeScript", + "emojiKey": null + } ] } ], @@ -47,4 +82,4 @@ "file": "examples/poll-side-by-side.json" } ] -} +} \ No newline at end of file diff --git a/data/updates/v0.8/update.json b/data/updates/v0.8/update.json index b962fce..900eaf7 100644 --- a/data/updates/v0.8/update.json +++ b/data/updates/v0.8/update.json @@ -3,7 +3,6 @@ "date": "2026-06-11", "title": "Framework & Architecture", "layout": "default", - "messageId": null, "sections": [ { "type": "new", @@ -12,7 +11,7 @@ "items": [ { "text": "`/tg poll mark-left` — mark a character as having left TG mid-game with 🪲 counter", "emojiKey": "cockroach" }, { "text": "Leave counter displays total times a character has left TG", "emojiKey": null }, - { "text": "W.Rank no-rank placeholder alignment — players without rank align correctly with ranked players", "emojiKey": "wrank_no_dash" } + { "text": "W.Rank no-rank placeholder alignment — players without rank now align correctly with ranked players", "emojiKey": null } ] }, { @@ -20,11 +19,8 @@ "label": "Improvements", "emoji": "⚡", "items": [ - { "text": "Config system moved from `.env` to `config.json` — hot-reloadable, no restart needed", "emojiKey": null }, - { "text": "All config organized into sections — `poll`, `wrank`, `channels`, `roles`, etc.", "emojiKey": null }, - { "text": "Scheduler plugin system — cron jobs are self-contained files, drop one in to add a job", "emojiKey": null }, - { "text": "Logger with levels and context icons — structured, filterable output", "emojiKey": null }, - { "text": "Benchmark profiling — vote path performance measured, optimized with `Promise.all`", "emojiKey": null } + { "text": "Voting is faster — poll embed and companion ephemeral now update in parallel", "emojiKey": null }, + { "text": "Config changes no longer require a bot restart — hot-reloadable from `config.json`", "emojiKey": null } ] }, { @@ -34,9 +30,12 @@ "items": [ { "text": "`Runtime` lifecycle system — phased startup: load → restore → connect → schedule → ready", "emojiKey": null }, { "text": "`Config` namespace — `Config.get({ section, key })` with full type safety per section", "emojiKey": null }, + { "text": "All config organized into sections — `poll`, `wrank`, `channels`, `roles`, etc.", "emojiKey": null }, { "text": "`Store` abstraction — centralized JSON file I/O with error handling", "emojiKey": null }, { "text": "`Paths` helper — no more `path.join(__dirname, ...)` scattered across the codebase", "emojiKey": null }, { "text": "`Discord` abstraction layer — `Discord.Interaction`, `Discord.Guild`, `Discord.Channel`", "emojiKey": null }, + { "text": "`Scheduler` plugin system — drop a file in `scheduler/` to add a cron job", "emojiKey": null }, + { "text": "`Logger` with levels and context icons — structured, filterable output", "emojiKey": null }, { "text": "`WRankEntry` hydration — runtime entries carry full `Character` object", "emojiKey": null }, { "text": "`Leaves` system — character leave tracking keyed by character name and history key", "emojiKey": null } ] @@ -50,4 +49,4 @@ "file": "examples/poll-leaves.json" } ] -} +} \ No newline at end of file diff --git a/src/discord/interaction.ts b/src/discord/interaction.ts index 12d98f6..0d04ecf 100644 --- a/src/discord/interaction.ts +++ b/src/discord/interaction.ts @@ -11,11 +11,18 @@ import { MessageFlags, } from "discord.js"; +import { Logger } from "@systems/logger"; +import { Benchmark } from "@systems/benchmark"; + type AnyInteraction = | ChatInputCommandInteraction | ButtonInteraction | ModalSubmitInteraction; +// ─── Logger ───────────────────────────────────────────────────────── + +const log = Logger.for("Discord.Interaction"); + // ─── Options resolver ───────────────────────────────────────────────────────── export interface OptionParams { @@ -80,10 +87,35 @@ async function followUp(interaction: AnyInteraction, params: ReplyParams): Promi }); } +async function deferReply(interaction: AnyInteraction, { ephemeral = false } = {}): Promise { + const bench = Benchmark.start("deferReply"); + try { + await interaction.deferReply({ + flags: ephemeral ? MessageFlags.Ephemeral : undefined + }); + bench.end(); + } catch (err: any) { + log.warn(`deferReply failed (interaction likely expired): ${err.message}`); + bench.end(); + throw err; + } +} + +async function editReply(interaction: AnyInteraction, params: ReplyParams | string): Promise { + const opts = typeof params === "string" ? { content: params } : params; + await interaction.editReply({ + content: opts.content, + embeds: opts.embeds, + components: opts.components, + }); +} + // ─── Namespace ──────────────────────────────────────────────────────────────── export const Interaction = { options, reply, followUp, + deferReply, + editReply }; diff --git a/src/subcommands/admin/updates.ts b/src/subcommands/admin/updates.ts index 0871de8..e77bc1e 100644 --- a/src/subcommands/admin/updates.ts +++ b/src/subcommands/admin/updates.ts @@ -1,28 +1,34 @@ import { ChatInputCommandInteraction } from "discord.js"; import { Updates } from "@systems/updates"; -import { replyAndDelete } from "@utils"; +import { Discord } from "@discord"; export async function handleUpdatesPost(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply({ ephemeral: true }); + await Discord.Interaction.deferReply(interaction, { ephemeral: true }); - const options = interaction.options as any; - const version = options.getString("version") ?? Updates.latest(); + const opts = Discord.Interaction.options(interaction); + const version = opts.string({ key: "version" }) ?? Updates.latest(); if (!version) { - await interaction.editReply("❌ No versions found."); + await Discord.Interaction.editReply(interaction, "❌ No versions found."); return; } - await Updates.post({ version, client: interaction.client }); - await interaction.editReply(`✅ Update \`${version}\` posted.`); + try { + await Updates.post({ version, client: interaction.client }); + await Discord.Interaction.editReply(interaction, `✅ Update \`${version}\` posted.`); + } catch (err: any) { + await Discord.Interaction.editReply(interaction, `❌ Failed: ${err.message}`); + } } export async function handleUpdatesPreview(interaction: ChatInputCommandInteraction): Promise { - const options = interaction.options as any; - const version = options.getString("version") ?? Updates.latest(); + await Discord.Interaction.deferReply(interaction, { ephemeral: true }); + + const opts = Discord.Interaction.options(interaction); + const version = opts.string({ key: "version" }) ?? Updates.latest(); if (!version) { - await interaction.reply({ content: "❌ No versions found.", ephemeral: true }); + await Discord.Interaction.editReply(interaction, "❌ No versions found."); return; } @@ -34,21 +40,20 @@ export async function handleUpdatesList(interaction: ChatInputCommandInteraction const latest = Updates.latest(); const lines = versions.map((v) => { const entry = Updates.get({ version: v }); - const posted = entry?.messageId ? "✅" : "⬜"; const tag = v === latest ? " ← latest" : ""; - return `${posted} \`${v}\` — ${entry?.title ?? ""}${tag}`; + return `⬜ \`${v}\` — ${entry?.title ?? ""}${tag}`; }); - await interaction.reply({ + await Discord.Interaction.reply(interaction, { content: lines.length > 0 ? lines.join("\n") : "No versions found.", ephemeral: true, }); } export async function autocompleteVersion(interaction: any): Promise { - const focused = interaction.options.getFocused().toLowerCase(); - const versions = Updates.list(); - const choices = versions + const focused = interaction.options.getFocused().toLowerCase(); + const versions = Updates.list(); + const choices = versions .filter((v) => v.toLowerCase().includes(focused)) .map((v) => { const entry = Updates.get({ version: v }); @@ -56,11 +61,4 @@ export async function autocompleteVersion(interaction: any): Promise { }) .slice(0, 25); await interaction.respond(choices); -} - -export const UpdatesCommands = { - post: handleUpdatesPost, - preview: handleUpdatesPreview, - list: handleUpdatesList, - autocomplete: autocompleteVersion, -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/systems/updates.ts b/src/systems/updates.ts index 2c70cda..c05a125 100644 --- a/src/systems/updates.ts +++ b/src/systems/updates.ts @@ -83,6 +83,8 @@ }[]; } + let _versionsCache: string[] | null = null; + // ─── Helpers ────────────────────────────────────────────────────────────────── function updatesDir(): string { @@ -195,10 +197,13 @@ // ─── Updates namespace ──────────────────────────────────────────────────────── export const Updates = { - list(): string[] { - const index = Store.read(path.join(updatesDir(), "versions.json")); - return index?.versions ?? []; - }, + list(): string[] { + if (!_versionsCache) { + const index = Store.read(path.join(updatesDir(), "versions.json")); + _versionsCache = index?.versions ?? []; + } + return _versionsCache; + }, latest(): string | null { const index = Store.read(path.join(updatesDir(), "versions.json"));