test: revert systems/updates.ts

This commit is contained in:
Nuno Duque Nunes 2026-06-12 16:56:57 +01:00
parent f97a77a5e9
commit 41c813661b

View file

@ -1,5 +1,5 @@
/** /**
* Updates manages bot changelog/update posts to #updates channel. * Updates manages bot changelog/update posts.
* *
* Usage: * Usage:
* import { Updates } from "@systems/updates"; * import { Updates } from "@systems/updates";
@ -11,9 +11,9 @@
* Updates.preview({ version: "v0.8", interaction }) * Updates.preview({ version: "v0.8", interaction })
*/ */
import fs from "fs";
import path from "path"; import path from "path";
import { Client, EmbedBuilder, TextChannel, MessageFlags, ChatInputCommandInteraction } from "discord.js"; import { Client, EmbedBuilder, TextChannel, MessageFlags } from "discord.js";
import { ChatInputCommandInteraction } from "discord.js";
import { Store } from "@systems/store"; import { Store } from "@systems/store";
import { Paths } from "@paths"; import { Paths } from "@paths";
import { Emoji } from "@systems/emojis"; import { Emoji } from "@systems/emojis";
@ -21,7 +21,7 @@
import { Logger } from "@systems/logger"; import { Logger } from "@systems/logger";
import { PollUI } from "@ui/poll"; import { PollUI } from "@ui/poll";
import { Nation, VoteEntry, PollState } from "@types"; import { Nation, VoteEntry, PollState } from "@types";
import { WRank } from "@systems/wrank"; import { WRank, WRankEntry } from "@systems/wrank";
import { Leaves } from "@systems/leaves"; import { Leaves } from "@systems/leaves";
const log = Logger.for("updates"); const log = Logger.for("updates");
@ -30,7 +30,7 @@
interface UpdateItem { interface UpdateItem {
text: string; text: string;
emojiKey?: string | null; emojiKey: string | null;
} }
interface UpdateSection { interface UpdateSection {
@ -52,6 +52,7 @@
date: string; date: string;
title: string; title: string;
layout: string; layout: string;
messageId: string | null;
sections: UpdateSection[]; sections: UpdateSection[];
examples: UpdateExample[]; examples: UpdateExample[];
} }
@ -82,7 +83,9 @@
}[]; }[];
} }
// ─── Paths ──────────────────────────────────────────────────────────────────── let _versionsCache: string[] | null = null;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function updatesDir(): string { function updatesDir(): string {
return Paths.data("updates"); return Paths.data("updates");
@ -92,80 +95,60 @@
return path.join(updatesDir(), version); return path.join(updatesDir(), version);
} }
function messageIdsPath(): string {
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 ───────────────────────────────────────────────────────────
let _versionsCache: string[] | null = null;
let _latestCache: string | null = null;
function loadVersionsIndex(): void {
if (_versionsCache) return;
const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json"));
_versionsCache = index?.versions ?? [];
_latestCache = index?.latest ?? null;
}
// ─── Embed building ───────────────────────────────────────────────────────────
function buildUpdateEmbed(entry: UpdateEntry): EmbedBuilder { function buildUpdateEmbed(entry: UpdateEntry): EmbedBuilder {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(`⚔️ The Arbiter — ${entry.version} · ${entry.date}`) .setTitle(`⚔️ The Arbiter — ${entry.version} · ${entry.date}`)
.setColor(0xe8a317) .setColor(0xe8a317)
.setTimestamp(); .setTimestamp();
// Build description from sections
const lines: string[] = []; const lines: string[] = [];
for (const section of entry.sections) { for (const section of entry.sections) {
lines.push(`${section.emoji} **${section.label}**`); lines.push(`${section.emoji} **${section.label}**`);
for (const item of section.items) { for (const item of section.items) {
const emojiStr = item.emojiKey ? (Emoji.get(item.emojiKey) || "•") : "•"; const emojiStr = item.emojiKey ? (Emoji.get(item.emojiKey) || "•") : "•";
const resolvedText = Emoji.resolveTokens(item.text); lines.push(`${emojiStr} ${item.text}`);
lines.push(`${emojiStr} ${resolvedText}`);
} }
lines.push(""); lines.push("");
} }
embed.setDescription(lines.join("\n").trim()); embed.setDescription(lines.join("\n").trim());
embed.setFooter({ text: `${entry.version}${entry.title}` }); embed.setFooter({ text: `${entry.version}${entry.title}` });
return embed; return embed;
} }
function buildExamplePollState(exampleData: ExamplePollState): PollState { function buildExamplePollState(exampleData: ExamplePollState): PollState {
// Build yes/no Maps
const yes = new Map<string, VoteEntry>(); const yes = new Map<string, VoteEntry>();
const no = new Map<string, VoteEntry>(); const no = new Map<string, VoteEntry>();
for (const entry of exampleData.yes) { for (const entry of exampleData.yes) {
yes.set(entry.userKey ?? entry.displayName ?? entry.characterName ?? "", entry); yes.set(entry.userKey ?? entry.displayName ?? entry.characterName ?? "", entry);
} }
for (const entry of (exampleData.no ?? [])) { for (const entry of exampleData.no) {
no.set(entry.userKey ?? entry.displayName ?? "", entry); no.set(entry.userKey ?? entry.displayName ?? "", entry);
} }
return { slot: exampleData.slot, locked: exampleData.locked, confirmed: exampleData.confirmed, yes, no }; return {
slot: exampleData.slot,
locked: exampleData.locked,
confirmed: exampleData.confirmed,
yes,
no,
};
} }
function injectMockWrank(exampleData: ExamplePollState): (() => void) | null { function injectMockWrank(exampleData: ExamplePollState): (() => void) | null {
if (!exampleData.wrank?.length) return null; if (!exampleData.wrank?.length) return null;
// We inject mock wrank entries temporarily into WRank's current week
// and return a cleanup function to restore the original state
const week = WRank.currentWeek(); const week = WRank.currentWeek();
const original = JSON.parse(JSON.stringify(week.entries)); const original = JSON.parse(JSON.stringify(week.entries));
// Temporarily replace entries with mock data
week.entries[Nation.Capella] = []; week.entries[Nation.Capella] = [];
week.entries[Nation.Procyon] = []; week.entries[Nation.Procyon] = [];
@ -183,6 +166,7 @@
}); });
} }
// Return cleanup function
return () => { return () => {
week.entries[Nation.Capella] = original[Nation.Capella] ?? []; week.entries[Nation.Capella] = original[Nation.Capella] ?? [];
week.entries[Nation.Procyon] = original[Nation.Procyon] ?? []; week.entries[Nation.Procyon] = original[Nation.Procyon] ?? [];
@ -191,11 +175,20 @@
function injectMockLeaves(exampleData: ExamplePollState): (() => void) | null { function injectMockLeaves(exampleData: ExamplePollState): (() => void) | null {
if (!exampleData.leaves?.length) return null; if (!exampleData.leaves?.length) return null;
// Mark leaves temporarily
for (const l of exampleData.leaves) { for (const l of exampleData.leaves) {
Leaves.mark({ characterName: l.characterName, ownerKey: "example", historyKey: l.historyKey as any, markedBy: "example" }); Leaves.mark({
characterName: l.characterName,
ownerKey: "example",
historyKey: l.historyKey as any,
markedBy: "example",
});
} }
return () => { return () => {
for (const l of exampleData.leaves!) { if (!exampleData.leaves) return;
for (const l of exampleData.leaves) {
Leaves.unmark({ characterName: l.characterName, historyKey: l.historyKey as any }); Leaves.unmark({ characterName: l.characterName, historyKey: l.historyKey as any });
} }
}; };
@ -205,35 +198,52 @@
export const Updates = { export const Updates = {
list(): string[] { list(): string[] {
loadVersionsIndex(); if (!_versionsCache) {
return _versionsCache ?? []; const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json"));
_versionsCache = index?.versions ?? [];
}
return _versionsCache;
}, },
latest(): string | null { latest(): string | null {
loadVersionsIndex(); const index = Store.read<VersionsIndex>(path.join(updatesDir(), "versions.json"));
return _latestCache; return index?.latest ?? null;
}, },
get({ version }: { version: string }): UpdateEntry | null { get({ version }: { version: string }): UpdateEntry | null {
return Store.read<UpdateEntry>(path.join(versionDir(version), "update.json")); return Store.read<UpdateEntry>(path.join(versionDir(version), "update.json"));
}, },
setMessageId({ version, messageId }: { version: string; messageId: string }): void {
const entry = Updates.get({ version });
if (!entry) return;
entry.messageId = messageId;
Store.write(path.join(versionDir(version), "update.json"), entry);
},
buildEmbeds(entry: UpdateEntry): EmbedBuilder[] { buildEmbeds(entry: UpdateEntry): EmbedBuilder[] {
const embeds: EmbedBuilder[] = [buildUpdateEmbed(entry)]; const embeds: EmbedBuilder[] = [buildUpdateEmbed(entry)];
// Build example embeds
for (const example of entry.examples) { for (const example of entry.examples) {
const examplePath = path.join(versionDir(entry.version), example.file); const examplePath = path.join(versionDir(entry.version), example.file);
const exampleData = Store.read<ExamplePollState>(examplePath); const exampleData = Store.read<ExamplePollState>(examplePath);
if (!exampleData) continue; if (!exampleData) continue;
// Inject mock data
const cleanupWrank = injectMockWrank(exampleData); const cleanupWrank = injectMockWrank(exampleData);
const cleanupLeaves = injectMockLeaves(exampleData); const cleanupLeaves = injectMockLeaves(exampleData);
try { try {
// Set the layout
PollUI.setLayout(example.layout); PollUI.setLayout(example.layout);
// Build the poll state
const state = buildExamplePollState(exampleData); const state = buildExamplePollState(exampleData);
const exampleEmbed = PollUI.buildEmbed(state, { overrideLockMsg: `🪲 ${example.caption}` });
exampleEmbed.setTitle(`📋 Example — ${example.caption}`); // Build embed using the real poll UI
const exampleEmbed = PollUI.buildEmbed(state, { overrideLockMsg: example.caption });
// exampleEmbed.setTitle(""); // no title for examples
embeds.push(exampleEmbed); embeds.push(exampleEmbed);
} finally { } finally {
cleanupWrank?.(); cleanupWrank?.();
@ -247,31 +257,36 @@
async post({ version, client }: { version: string; client: Client }): Promise<void> { async post({ version, client }: { version: string; client: Client }): Promise<void> {
const entry = Updates.get({ version }); const entry = Updates.get({ version });
if (!entry) { log.error(`Version ${version} not found`); return; } if (!entry) {
log.error(`Version ${version} not found`);
return;
}
const channelId = Config.get({ section: "channels", key: "updates" }); const channelId = Config.get({ section: "channels", key: "updates" });
if (!channelId) { log.error("updates channel not configured"); return; } if (!channelId) {
log.error("updates channel not configured");
return;
}
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);
log.debug(`post: version=${version} messageId=${messageId} idsPath=${messageIdsPath()}`); if (entry.messageId) {
// Edit existing message
if (messageId) {
try { try {
const msg = await channel.messages.fetch(messageId); const msg = await channel.messages.fetch(entry.messageId);
await msg.edit({ embeds }); await msg.edit({ embeds });
log.info(`Edited ${version} (${messageId})`); log.info(`Edited update ${version} (${entry.messageId})`);
return; return;
} catch { } catch {
log.warn(`Could not edit ${messageId}, posting new`); log.warn(`Could not edit message ${entry.messageId}, posting new`);
} }
} }
// Post new message
const msg = await channel.send({ embeds }); const msg = await channel.send({ embeds });
saveMessageId(version, msg.id); Updates.setMessageId({ version, messageId: msg.id });
log.info(`Posted ${version} (${msg.id})`); log.info(`Posted update ${version} (${msg.id})`);
}, },
async preview({ version, interaction }: { async preview({ version, interaction }: {
@ -280,10 +295,11 @@
}): Promise<void> { }): Promise<void> {
const entry = Updates.get({ version }); const entry = Updates.get({ version });
if (!entry) { if (!entry) {
await interaction.editReply("❌ Version not found."); await interaction.reply({ content: `❌ Version \`${version}\` not found.`, flags: MessageFlags.Ephemeral });
return; return;
} }
const embeds = Updates.buildEmbeds(entry); const embeds = Updates.buildEmbeds(entry);
await interaction.editReply({ embeds }); await interaction.reply({ embeds, flags: MessageFlags.Ephemeral });
}, },
}; };