refactor update messageId system / Discord.Interaction refactors

This commit is contained in:
Nuno Duque Nunes 2026-06-12 16:27:36 +01:00
parent be84fa2fb6
commit 666986afb1
11 changed files with 336 additions and 120 deletions

View file

@ -1,23 +1,46 @@
{ {
"version": "v0.1", "version": "v0.1",
"date": "2026-05-01", "date": "2026-05-28",
"title": "Core Poll System", "title": "Core Poll System",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "Poll creation with `/tg poll start` — opens voting for the upcoming TG",
{ "text": "Yes/No voting with character display — class emoji, level and name", "emojiKey": "wi" }, "emojiKey": null
{ "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": "Poll scheduling with cronjobs — opens voting for the upcoming TGs at specific times",
{ "text": "W.Rank display — rank number with gold variant for 7 TGs done", "emojiKey": "wrank_1_gold" }, "emojiKey": null
{ "text": "Bringer display — Storm Bringer and Luminous Bringer indicators", "emojiKey": "storm_bringer" } },
{
"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"
}
] ]
} }
], ],

View file

@ -1,22 +1,42 @@
{ {
"version": "v0.2", "version": "v0.2",
"date": "2026-05-10", "date": "2026-05-30",
"title": "Character System & Impersonation", "title": "Character System & Impersonation",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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 management — `/tg char add/remove/set-active/set-nation`",
{ "text": "Character borrowing — request to play someone else's character", "emojiKey": "borrowed" }, "emojiKey": "active_char"
{ "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": "Character sharing — lend your character to another player with `/tg char share`",
{ "text": "Autocomplete for character names with nation emoji and shared indicator 🔗", "emojiKey": null } "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,9 +44,18 @@
"label": "Under the Hood", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "items": [
{ "text": "Character data stored per user in `characters.json`", "emojiKey": null }, {
{ "text": "Borrow requests tracked with expiry timestamps", "emojiKey": null }, "text": "Character data stored per user in `characters.json`",
{ "text": "ID-first usermap lookup — survives Discord username changes", "emojiKey": null } "emojiKey": null
},
{
"text": "Borrow requests tracked with expiry timestamps",
"emojiKey": null
},
{
"text": "ID-first usermap lookup — survives Discord username changes",
"emojiKey": null
}
] ]
} }
], ],

View file

@ -1,19 +1,30 @@
{ {
"version": "v0.3", "version": "v0.3",
"date": "2026-05-18", "date": "2026-06-01",
"title": "Conflict Resolution", "title": "Conflict Resolution",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "Character conflict detection — warns when two players want the same character",
{ "text": "Reclaim notifies the borrower via DM with a character selection prompt", "emojiKey": null }, "emojiKey": null
{ "text": "Auto-vote on conflict switch — voting Yes automatically after switching", "emojiKey": "active_char" } },
{
"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", "label": "Bug Fixes",
"emoji": "🔧", "emoji": "🔧",
"items": [ "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,8 +47,14 @@
"label": "Under the Hood", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "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
}
] ]
} }
], ],

View file

@ -1,18 +1,26 @@
{ {
"version": "v0.4", "version": "v0.4",
"date": "2026-05-25", "date": "2026-06-03",
"title": "Companion System", "title": "Companion System",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "Companion ephemeral — after voting Yes, shows your active character with switch buttons",
{ "text": "Switching to a taken character triggers the conflict resolution flow", "emojiKey": null } "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", "label": "Improvements",
"emoji": "⚡", "emoji": "⚡",
"items": [ "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,8 +43,14 @@
"label": "Under the Hood", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "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
}
] ]
} }
], ],

View file

@ -1,19 +1,30 @@
{ {
"version": "v0.5", "version": "v0.5",
"date": "2026-06-01", "date": "2026-06-05",
"title": "W.Rank Improvements", "title": "W.Rank Improvements",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "W.Rank delta system — tracks rank movement with <:wrank_up:1512114414474756132><:wrank_down:1511906547104616643> and grey placeholder for unchanged",
{ "text": "Weekly reset carries Bringer forward — W.Rank 1 with goal TGs becomes next week's Bringer", "emojiKey": "storm_bringer" }, "emojiKey": "wrank_up"
{ "text": "No-rank placeholder alignment — players without W.Rank align with those who have it", "emojiKey": "wrank_no_dash" } },
{
"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", "label": "Improvements",
"emoji": "⚡", "emoji": "⚡",
"items": [ "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", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "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": "`lastRankChangeAt` timestamp on W.Rank entries — drives the 24h snapshot window",
{ "text": "`WRankEntry` hydration — runtime entries carry full `Character` object, not just flat fields", "emojiKey": null } "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
}
] ]
} }
], ],

View file

@ -1,19 +1,30 @@
{ {
"version": "v0.6", "version": "v0.6",
"date": "2026-06-05", "date": "2026-06-07",
"title": "Administration & Score Submission", "title": "Administration & Score Submission",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "`/tg-admin user map/unmap/list` — register Discord accounts to player profiles",
{ "text": "Score submission via Submit Score button after TG ends", "emojiKey": null }, "emojiKey": null
{ "text": "`/tg score get` — retrieve your score for a specific TG slot", "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", "label": "Improvements",
"emoji": "⚡", "emoji": "⚡",
"items": [ "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,9 +47,18 @@
"label": "Under the Hood", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "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": "`CharacterRegistry` — cached character lookups across all users",
{ "text": "`Score` namespace — centralized score submission and retrieval", "emojiKey": null } "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
}
] ]
} }
], ],

View file

@ -1,19 +1,30 @@
{ {
"version": "v0.7", "version": "v0.7",
"date": "2026-06-08", "date": "2026-06-09",
"title": "UI Layout System", "title": "UI Layout System",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
"label": "New Features", "label": "New Features",
"emoji": "✨", "emoji": "✨",
"items": [ "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": "Poll layout system — multiple display styles, switchable via `/tg-config poll set-layout`",
{ "text": "`side-by-side` layout — nations displayed inline, auto-stacks when >5 players per nation", "emojiKey": null }, "emojiKey": null
{ "text": "Layout persists across bot restarts", "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", "label": "Improvements",
"emoji": "⚡", "emoji": "⚡",
"items": [ "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": "Voting Yes and No now run in parallel — faster poll embed updates",
{ "text": "Character class now carries full name and emoji — `Force Blader`, `Wizard`, etc.", "emojiKey": null } "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", "label": "Under the Hood",
"emoji": "🔩", "emoji": "🔩",
"items": [ "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": "`BaseLayout` — shared functions inherited by all layouts, override only what differs",
{ "text": "`Character` type now carries `ownerKey` and full `CharacterClass` object", "emojiKey": null }, "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": "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
}
] ]
} }
], ],

View file

@ -3,7 +3,6 @@
"date": "2026-06-11", "date": "2026-06-11",
"title": "Framework & Architecture", "title": "Framework & Architecture",
"layout": "default", "layout": "default",
"messageId": null,
"sections": [ "sections": [
{ {
"type": "new", "type": "new",
@ -12,7 +11,7 @@
"items": [ "items": [
{ "text": "`/tg poll mark-left` — mark a character as having left TG mid-game with 🪲 counter", "emojiKey": "cockroach" }, { "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": "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", "label": "Improvements",
"emoji": "⚡", "emoji": "⚡",
"items": [ "items": [
{ "text": "Config system moved from `.env` to `config.json` — hot-reloadable, no restart needed", "emojiKey": null }, { "text": "Voting is faster — poll embed and companion ephemeral now update in parallel", "emojiKey": null },
{ "text": "All config organized into sections — `poll`, `wrank`, `channels`, `roles`, etc.", "emojiKey": null }, { "text": "Config changes no longer require a bot restart — hot-reloadable from `config.json`", "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 }
] ]
}, },
{ {
@ -34,9 +30,12 @@
"items": [ "items": [
{ "text": "`Runtime` lifecycle system — phased startup: load → restore → connect → schedule → ready", "emojiKey": null }, { "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": "`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": "`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": "`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": "`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": "`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 } { "text": "`Leaves` system — character leave tracking keyed by character name and history key", "emojiKey": null }
] ]

View file

@ -11,11 +11,18 @@ import {
MessageFlags, MessageFlags,
} from "discord.js"; } from "discord.js";
import { Logger } from "@systems/logger";
import { Benchmark } from "@systems/benchmark";
type AnyInteraction = type AnyInteraction =
| ChatInputCommandInteraction | ChatInputCommandInteraction
| ButtonInteraction | ButtonInteraction
| ModalSubmitInteraction; | ModalSubmitInteraction;
// ─── Logger ─────────────────────────────────────────────────────────
const log = Logger.for("Discord.Interaction");
// ─── Options resolver ───────────────────────────────────────────────────────── // ─── Options resolver ─────────────────────────────────────────────────────────
export interface OptionParams { export interface OptionParams {
@ -80,10 +87,35 @@ async function followUp(interaction: AnyInteraction, params: ReplyParams): Promi
}); });
} }
async function deferReply(interaction: AnyInteraction, { ephemeral = false } = {}): Promise<void> {
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<void> {
const opts = typeof params === "string" ? { content: params } : params;
await interaction.editReply({
content: opts.content,
embeds: opts.embeds,
components: opts.components,
});
}
// ─── Namespace ──────────────────────────────────────────────────────────────── // ─── Namespace ────────────────────────────────────────────────────────────────
export const Interaction = { export const Interaction = {
options, options,
reply, reply,
followUp, followUp,
deferReply,
editReply
}; };

View file

@ -1,28 +1,34 @@
import { ChatInputCommandInteraction } from "discord.js"; import { ChatInputCommandInteraction } from "discord.js";
import { Updates } from "@systems/updates"; import { Updates } from "@systems/updates";
import { replyAndDelete } from "@utils"; import { Discord } from "@discord";
export async function handleUpdatesPost(interaction: ChatInputCommandInteraction): Promise<void> { export async function handleUpdatesPost(interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.deferReply({ ephemeral: true }); await Discord.Interaction.deferReply(interaction, { ephemeral: true });
const options = interaction.options as any; const opts = Discord.Interaction.options(interaction);
const version = options.getString("version") ?? Updates.latest(); const version = opts.string({ key: "version" }) ?? Updates.latest();
if (!version) { if (!version) {
await interaction.editReply("❌ No versions found."); await Discord.Interaction.editReply(interaction, "❌ No versions found.");
return; return;
} }
await Updates.post({ version, client: interaction.client }); try {
await interaction.editReply(`✅ Update \`${version}\` posted.`); 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<void> { export async function handleUpdatesPreview(interaction: ChatInputCommandInteraction): Promise<void> {
const options = interaction.options as any; await Discord.Interaction.deferReply(interaction, { ephemeral: true });
const version = options.getString("version") ?? Updates.latest();
const opts = Discord.Interaction.options(interaction);
const version = opts.string({ key: "version" }) ?? Updates.latest();
if (!version) { if (!version) {
await interaction.reply({ content: "❌ No versions found.", ephemeral: true }); await Discord.Interaction.editReply(interaction, "❌ No versions found.");
return; return;
} }
@ -34,21 +40,20 @@ export async function handleUpdatesList(interaction: ChatInputCommandInteraction
const latest = Updates.latest(); const latest = Updates.latest();
const lines = versions.map((v) => { const lines = versions.map((v) => {
const entry = Updates.get({ version: v }); const entry = Updates.get({ version: v });
const posted = entry?.messageId ? "✅" : "⬜";
const tag = v === latest ? " ← latest" : ""; 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.", content: lines.length > 0 ? lines.join("\n") : "No versions found.",
ephemeral: true, ephemeral: true,
}); });
} }
export async function autocompleteVersion(interaction: any): Promise<void> { export async function autocompleteVersion(interaction: any): Promise<void> {
const focused = interaction.options.getFocused().toLowerCase(); const focused = interaction.options.getFocused().toLowerCase();
const versions = Updates.list(); const versions = Updates.list();
const choices = versions const choices = versions
.filter((v) => v.toLowerCase().includes(focused)) .filter((v) => v.toLowerCase().includes(focused))
.map((v) => { .map((v) => {
const entry = Updates.get({ version: v }); const entry = Updates.get({ version: v });
@ -57,10 +62,3 @@ export async function autocompleteVersion(interaction: any): Promise<void> {
.slice(0, 25); .slice(0, 25);
await interaction.respond(choices); await interaction.respond(choices);
} }
export const UpdatesCommands = {
post: handleUpdatesPost,
preview: handleUpdatesPreview,
list: handleUpdatesList,
autocomplete: autocompleteVersion,
};

View file

@ -83,6 +83,8 @@
}[]; }[];
} }
let _versionsCache: string[] | null = null;
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
function updatesDir(): string { function updatesDir(): string {
@ -195,10 +197,13 @@
// ─── Updates namespace ──────────────────────────────────────────────────────── // ─── Updates namespace ────────────────────────────────────────────────────────
export const Updates = { export const Updates = {
list(): string[] { list(): string[] {
const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json")); if (!_versionsCache) {
return index?.versions ?? []; const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json"));
}, _versionsCache = index?.versions ?? [];
}
return _versionsCache;
},
latest(): string | null { latest(): string | null {
const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json")); const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json"));