test: revert systems/updates.ts
This commit is contained in:
parent
f97a77a5e9
commit
41c813661b
1 changed files with 96 additions and 80 deletions
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Loading…
Add table
Reference in a new issue