gistfile1.txt
· 12 KiB · Text
原始文件
import {
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
TextChannel,
GuildMember,
} from "discord.js";
import { PollState, VoteEntry, Nation, TGSlot } from "@src/types";
import { cfg } from "@systems/config";
import { getEmoji, getClassEmoji, getNationEmoji } from "@systems/emojis";
import { getActiveCharacter, getCharacterByName } from "@systems/characters";
import { resolveNation } from "@systems/nations";
import { getEntry, getBringer } from "@systems/wrank";
import { nowFormatted } from "@systems/messages";
import { format } from "@format";
import { persist } from "@systems/pollPersistence"
import { clearSessionBorrows } from "@systems/borrow";
import { clearAllImpersonations } from "@systems/impersonate";
// ─── Poll state ───────────────────────────────────────────────────────────────
export const polls: Map<number, PollState> = new Map();
const publicOverrides: Map<string, { yes?: string; no?: string }> = new Map();
const ephemeralOverrides: Map<string, { yes?: string; no?: string }> = new Map();
export function setPublicOverride(userId: string, voteType: "yes" | "no", message: string): void {
const e = publicOverrides.get(userId) ?? {};
e[voteType] = message;
publicOverrides.set(userId, e);
}
export function clearPublicOverride(userId: string, voteType?: "yes" | "no"): void {
if (!voteType) { publicOverrides.delete(userId); return; }
const e = publicOverrides.get(userId);
if (e) delete e[voteType];
}
export function setEphemeralOverride(userId: string, voteType: "yes" | "no", message: string): void {
const e = ephemeralOverrides.get(userId) ?? {};
e[voteType] = message;
ephemeralOverrides.set(userId, e);
}
export function clearEphemeralOverride(userId: string, voteType?: "yes" | "no"): void {
if (!voteType) { ephemeralOverrides.delete(userId); return; }
const e = ephemeralOverrides.get(userId);
if (e) delete e[voteType];
}
export function getPublicOverride(userId: string, voteType: "yes" | "no"): string | undefined {
return publicOverrides.get(userId)?.[voteType];
}
export function getEphemeralOverride(userId: string, voteType: "yes" | "no"): string | undefined {
return ephemeralOverrides.get(userId)?.[voteType];
}
export function resetPollOverrides(): void {
publicOverrides.clear();
ephemeralOverrides.clear();
}
export function lockPoll(slot: number): void {
const state = polls.get(slot);
if (!state) return;
state.locked = true;
// Snapshot the userKeys that were in yes at lock time
state.lockedYesKeys = new Set(
[...state.yes.values()]
.map((e) => e.userKey)
.filter((k): k is string => !!k)
);
persist.save(polls)
}
// ─── Character display ────────────────────────────────────────────────────────
function getNationBringerTitle(nation: Nation) {
const stormBringerIcon = getEmoji("storm_bringer") || "⚡";
const stormBringer = `${stormBringerIcon}`;
const luminousBringerIcon = getEmoji("luminous_bringer") || "🔆";
const luminousBringer = `${luminousBringerIcon}` || `🔆 Luminous Bringer`;
const nationMap = {
"Capella": luminousBringer,
"Procyon": stormBringer
};
return nationMap[nation];
}
function getBringerDisplay(nation: Nation): string {
const bringerMap: Record<Nation, string> = {
Capella: getEmoji("luminous_bringer") || "🔆 Luminous Bringer",
Procyon: getEmoji("storm_bringer") || "⚡ Storm Bringer",
};
return bringerMap[nation];
}
function formatCharRow(entry: VoteEntry, showNationEmoji = false, nationHasRank = false): string {
const cfgFormat = cfg("charDisplayFormat");
const nation = entry.characterNation;
const wRankEntry = entry.characterName && entry.characterNation
? getEntry(entry.characterName, entry.characterNation)
: null;
let wrank = "";
if (wRankEntry) {
const wRankGoal = cfg("wRankGoal");
wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true });
} else if (nationHasRank) {
wrank = format.wrank.noRank();
}
const classStr = entry.characterClass
? (getClassEmoji(entry.characterClass) || entry.characterClass)
: "";
const levelStr = entry.characterLevel && cfg("showLevelInMessages" as any)
? `${entry.characterLevel}`
: "";
let row = cfgFormat
.replace("{wrank}", wrank)
.replace("{class}", classStr)
.replace("{level}", levelStr)
.replace("{name}", entry.characterName ?? entry.displayName)
.replace(/\s+/g, " ")
.trim();
// Bringer title — independent of W.Rank so override always shows
if (nation && entry.userKey) {
const bringer = getBringer(nation);
if (bringer && bringer === entry.characterName) {
row += ` · ${getBringerDisplay(nation)}`;
}
}
if (entry.borrowedFrom) {
row += ` ${getEmoji("borrowed") || "🔗"}`;
}
if (showNationEmoji && nation) row = `${getNationEmoji(nation)} ${row}`;
return row;
}
// ─── Embed building ───────────────────────────────────────────────────────────
export function buildEmbed(state: PollState, overrideLockMsg?: string): EmbedBuilder {
const yesByNation = { Capella: [] as VoteEntry[], Procyon: [] as VoteEntry[] };
const noVoters: VoteEntry[] = [];
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
const showNoInline = (cfg as any)("showNoInNationField") ?? false;
for (const entry of state.yes.values()) {
const nation = entry.characterNation ?? "Capella";
yesByNation[nation].push(entry);
allMessages.push({ entry, voteType: "yes" });
}
for (const entry of state.no.values()) {
noVoters.push(entry);
allMessages.push({ entry, voteType: "no" });
}
const capellaEmoji = getEmoji("capella");
const procyonEmoji = getEmoji("procyon");
const formatNationField = (nation: Nation): string => {
const yesEntries = yesByNation[nation];
const hasRank = yesEntries.some((e) => e.characterName && getEntry(e.characterName, nation) !== null);
const noEntries = showNoInline
? noVoters.filter((e) => e.characterNation === nation)
: [];
const lines = [
...yesEntries.map((e) => formatCharRow(e, false, hasRank)),
...noEntries.map((e) => `❌ ${formatCharRow(e, false, hasRank)}`),
];
return lines.length > 0 ? lines.join("\n") : "—";
};
const formatMessages = (): string => {
if (allMessages.length === 0) return "";
return allMessages
.map((m) => {
const name = m.entry.characterName ?? m.entry.displayName;
const prefix = m.voteType === "no" ? "✗ " : "✓ ";
const msg = m.entry.publicMessage ? ` — ${m.entry.publicMessage}` : "";
return `${prefix}${name} · ${m.entry.votedAt}${msg}`;
})
.join("\n");
};
const locked = state.locked;
const confirmed = state.confirmed;
const color =
confirmed === "yes" ? 0x57f287 :
confirmed === "no" ? 0xed4245 :
locked ? 0x888888 :
0xe8a317;
// Title with nation + no counts (hidden when confirmed or locked)
const counts = !locked && confirmed === null
? ` ${capellaEmoji} ${yesByNation.Capella.length} ${procyonEmoji} ${yesByNation.Procyon.length}`
: "";
const statusSuffix =
locked ? " 🔒" :
confirmed === "yes" ? " ✅" :
confirmed === "no" ? " ❌" : "";
const title = `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`;
const embed = new EmbedBuilder()
.setTitle(title)
.setColor(color)
.addFields(
{ name: `${capellaEmoji} Capella (${yesByNation.Capella.length})`, value: formatNationField("Capella"), inline: false },
{ name: "\u200b", value: "\u200b", inline: false },
{ name: `${procyonEmoji} Procyon (${yesByNation.Procyon.length})`, value: formatNationField("Procyon"), inline: false },
)
.setTimestamp();
const msgSection = formatMessages();
if (msgSection) {
embed.addFields({ name: "\u200b", value: msgSection, inline: false });
}
let footer: string;
if (confirmed === "yes") footer = cfg("confirmYesMessage");
else if (confirmed === "no") footer = cfg("confirmNoMessage");
else if (locked) footer = overrideLockMsg ?? cfg("lockMessage");
else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`;
embed.setFooter({ text: footer });
return embed;
}
export function buildButtons(
disabled: boolean,
showSubmit?: boolean
): ActionRowBuilder<ButtonBuilder>[] {
if (showSubmit) {
const scoreEmoji = getEmoji("score");
const submitBtn = new ButtonBuilder()
.setCustomId("tg_score_submit")
.setLabel("Submit Score")
.setStyle(ButtonStyle.Secondary);
if (scoreEmoji) submitBtn.setEmoji(format.emoji(scoreEmoji) ?? scoreEmoji);
return [new ActionRowBuilder<ButtonBuilder>().addComponents(submitBtn)];
}
const yesBtn = new ButtonBuilder()
.setCustomId("tg_yes").setLabel("✅ Yes").setStyle(ButtonStyle.Success).setDisabled(disabled);
const noBtn = new ButtonBuilder()
.setCustomId("tg_no").setLabel("❌ No").setStyle(ButtonStyle.Danger).setDisabled(disabled);
return [new ActionRowBuilder<ButtonBuilder>().addComponents(yesBtn, noBtn)];
}
export async function updatePollMessage(
channel: TextChannel,
slot: number,
overrideLockMsg?: string,
showSubmit?: boolean
): Promise<void> {
const state = polls.get(slot);
if (!state?.messageId) return;
console.log(`[updatePollMessage] slot=${slot} showSubmit=${showSubmit} messageId=${state.messageId}`);
const buttons = buildButtons(state.locked || state.confirmed !== null, showSubmit);
console.log(`[updatePollMessage] components rows=${buttons.length}`);
try {
const msg = await channel.messages.fetch(state.messageId);
await msg.edit({ embeds: [buildEmbed(state, overrideLockMsg)], components: buttons });
} catch (err) {
console.error("Failed to update poll message:", err);
}
}
export async function postPoll(channel: TextChannel, slot: TGSlot): Promise<void> {
resetPollOverrides();
persist.clear();
clearSessionBorrows();
clearAllImpersonations();
const state: PollState = {
messageId: null, slot: slot.tgHour,
yes: new Map(), no: new Map(),
locked: false, confirmed: null,
};
polls.set(slot.tgHour, state);
const msg = await channel.send({ embeds: [buildEmbed(state)], components: buildButtons(false) });
state.messageId = msg.id;
console.log(`[${new Date().toISOString()}] Poll posted for ${slot.tgHour}:00.`);
persist.save(polls)
}
export function createVoteEntry(
userId: string,
member: GuildMember,
userKey: string | null,
discordUsername: string
): Omit<VoteEntry, "votedAt" | "previousYesAt" | "previousNoAt" | "publicMessage"> {
const serverNickname = member.nickname ?? null;
const globalNickname = member.user.globalName ?? null;
const displayName = serverNickname ?? globalNickname ?? discordUsername;
const { getEffectiveCharacter } = require("@systems/borrow");
const { char, borrowedFrom: bf } = userKey
? getEffectiveCharacter(userKey)
: { char: null, borrowedFrom: null };
console.log(`[createVoteEntry] userKey=${userKey} char=${char?.name} borrowedFrom=${bf}`);
return {
userKey: userKey ?? (undefined as any),
displayName,
characterName: char?.name,
characterClass: char?.class,
characterLevel: char?.level,
characterNation: char?.nation ?? (resolveNation(member, userKey) ?? undefined),
borrowedFrom: bf ?? undefined,
};
}
| 1 | import { |
| 2 | EmbedBuilder, |
| 3 | ButtonBuilder, |
| 4 | ButtonStyle, |
| 5 | ActionRowBuilder, |
| 6 | TextChannel, |
| 7 | GuildMember, |
| 8 | } from "discord.js"; |
| 9 | import { PollState, VoteEntry, Nation, TGSlot } from "@src/types"; |
| 10 | import { cfg } from "@systems/config"; |
| 11 | import { getEmoji, getClassEmoji, getNationEmoji } from "@systems/emojis"; |
| 12 | import { getActiveCharacter, getCharacterByName } from "@systems/characters"; |
| 13 | import { resolveNation } from "@systems/nations"; |
| 14 | import { getEntry, getBringer } from "@systems/wrank"; |
| 15 | import { nowFormatted } from "@systems/messages"; |
| 16 | import { format } from "@format"; |
| 17 | import { persist } from "@systems/pollPersistence" |
| 18 | import { clearSessionBorrows } from "@systems/borrow"; |
| 19 | import { clearAllImpersonations } from "@systems/impersonate"; |
| 20 | |
| 21 | // ─── Poll state ─────────────────────────────────────────────────────────────── |
| 22 | export const polls: Map<number, PollState> = new Map(); |
| 23 | |
| 24 | const publicOverrides: Map<string, { yes?: string; no?: string }> = new Map(); |
| 25 | const ephemeralOverrides: Map<string, { yes?: string; no?: string }> = new Map(); |
| 26 | |
| 27 | export function setPublicOverride(userId: string, voteType: "yes" | "no", message: string): void { |
| 28 | const e = publicOverrides.get(userId) ?? {}; |
| 29 | e[voteType] = message; |
| 30 | publicOverrides.set(userId, e); |
| 31 | } |
| 32 | export function clearPublicOverride(userId: string, voteType?: "yes" | "no"): void { |
| 33 | if (!voteType) { publicOverrides.delete(userId); return; } |
| 34 | const e = publicOverrides.get(userId); |
| 35 | if (e) delete e[voteType]; |
| 36 | } |
| 37 | export function setEphemeralOverride(userId: string, voteType: "yes" | "no", message: string): void { |
| 38 | const e = ephemeralOverrides.get(userId) ?? {}; |
| 39 | e[voteType] = message; |
| 40 | ephemeralOverrides.set(userId, e); |
| 41 | } |
| 42 | export function clearEphemeralOverride(userId: string, voteType?: "yes" | "no"): void { |
| 43 | if (!voteType) { ephemeralOverrides.delete(userId); return; } |
| 44 | const e = ephemeralOverrides.get(userId); |
| 45 | if (e) delete e[voteType]; |
| 46 | } |
| 47 | export function getPublicOverride(userId: string, voteType: "yes" | "no"): string | undefined { |
| 48 | return publicOverrides.get(userId)?.[voteType]; |
| 49 | } |
| 50 | export function getEphemeralOverride(userId: string, voteType: "yes" | "no"): string | undefined { |
| 51 | return ephemeralOverrides.get(userId)?.[voteType]; |
| 52 | } |
| 53 | export function resetPollOverrides(): void { |
| 54 | publicOverrides.clear(); |
| 55 | ephemeralOverrides.clear(); |
| 56 | } |
| 57 | |
| 58 | export function lockPoll(slot: number): void { |
| 59 | const state = polls.get(slot); |
| 60 | if (!state) return; |
| 61 | state.locked = true; |
| 62 | |
| 63 | // Snapshot the userKeys that were in yes at lock time |
| 64 | state.lockedYesKeys = new Set( |
| 65 | [...state.yes.values()] |
| 66 | .map((e) => e.userKey) |
| 67 | .filter((k): k is string => !!k) |
| 68 | ); |
| 69 | |
| 70 | persist.save(polls) |
| 71 | } |
| 72 | |
| 73 | |
| 74 | // ─── Character display ──────────────────────────────────────────────────────── |
| 75 | function getNationBringerTitle(nation: Nation) { |
| 76 | const stormBringerIcon = getEmoji("storm_bringer") || "⚡"; |
| 77 | const stormBringer = `${stormBringerIcon}`; |
| 78 | |
| 79 | const luminousBringerIcon = getEmoji("luminous_bringer") || "🔆"; |
| 80 | const luminousBringer = `${luminousBringerIcon}` || `🔆 Luminous Bringer`; |
| 81 | |
| 82 | const nationMap = { |
| 83 | "Capella": luminousBringer, |
| 84 | "Procyon": stormBringer |
| 85 | }; |
| 86 | |
| 87 | return nationMap[nation]; |
| 88 | } |
| 89 | |
| 90 | function getBringerDisplay(nation: Nation): string { |
| 91 | const bringerMap: Record<Nation, string> = { |
| 92 | Capella: getEmoji("luminous_bringer") || "🔆 Luminous Bringer", |
| 93 | Procyon: getEmoji("storm_bringer") || "⚡ Storm Bringer", |
| 94 | }; |
| 95 | return bringerMap[nation]; |
| 96 | } |
| 97 | |
| 98 | function formatCharRow(entry: VoteEntry, showNationEmoji = false, nationHasRank = false): string { |
| 99 | const cfgFormat = cfg("charDisplayFormat"); |
| 100 | const nation = entry.characterNation; |
| 101 | const wRankEntry = entry.characterName && entry.characterNation |
| 102 | ? getEntry(entry.characterName, entry.characterNation) |
| 103 | : null; |
| 104 | |
| 105 | let wrank = ""; |
| 106 | if (wRankEntry) { |
| 107 | const wRankGoal = cfg("wRankGoal"); |
| 108 | wrank = format.wrank.full(wRankEntry, { goal: wRankGoal, brackets: true }); |
| 109 | } else if (nationHasRank) { |
| 110 | wrank = format.wrank.noRank(); |
| 111 | } |
| 112 | |
| 113 | const classStr = entry.characterClass |
| 114 | ? (getClassEmoji(entry.characterClass) || entry.characterClass) |
| 115 | : ""; |
| 116 | |
| 117 | const levelStr = entry.characterLevel && cfg("showLevelInMessages" as any) |
| 118 | ? `${entry.characterLevel}` |
| 119 | : ""; |
| 120 | |
| 121 | let row = cfgFormat |
| 122 | .replace("{wrank}", wrank) |
| 123 | .replace("{class}", classStr) |
| 124 | .replace("{level}", levelStr) |
| 125 | .replace("{name}", entry.characterName ?? entry.displayName) |
| 126 | .replace(/\s+/g, " ") |
| 127 | .trim(); |
| 128 | |
| 129 | // Bringer title — independent of W.Rank so override always shows |
| 130 | if (nation && entry.userKey) { |
| 131 | const bringer = getBringer(nation); |
| 132 | if (bringer && bringer === entry.characterName) { |
| 133 | row += ` · ${getBringerDisplay(nation)}`; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | if (entry.borrowedFrom) { |
| 138 | row += ` ${getEmoji("borrowed") || "🔗"}`; |
| 139 | } |
| 140 | |
| 141 | if (showNationEmoji && nation) row = `${getNationEmoji(nation)} ${row}`; |
| 142 | |
| 143 | return row; |
| 144 | } |
| 145 | |
| 146 | // ─── Embed building ─────────────────────────────────────────────────────────── |
| 147 | export function buildEmbed(state: PollState, overrideLockMsg?: string): EmbedBuilder { |
| 148 | const yesByNation = { Capella: [] as VoteEntry[], Procyon: [] as VoteEntry[] }; |
| 149 | const noVoters: VoteEntry[] = []; |
| 150 | const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = []; |
| 151 | const showNoInline = (cfg as any)("showNoInNationField") ?? false; |
| 152 | |
| 153 | for (const entry of state.yes.values()) { |
| 154 | const nation = entry.characterNation ?? "Capella"; |
| 155 | yesByNation[nation].push(entry); |
| 156 | allMessages.push({ entry, voteType: "yes" }); |
| 157 | } |
| 158 | for (const entry of state.no.values()) { |
| 159 | noVoters.push(entry); |
| 160 | allMessages.push({ entry, voteType: "no" }); |
| 161 | } |
| 162 | |
| 163 | const capellaEmoji = getEmoji("capella"); |
| 164 | const procyonEmoji = getEmoji("procyon"); |
| 165 | |
| 166 | const formatNationField = (nation: Nation): string => { |
| 167 | const yesEntries = yesByNation[nation]; |
| 168 | const hasRank = yesEntries.some((e) => e.characterName && getEntry(e.characterName, nation) !== null); |
| 169 | const noEntries = showNoInline |
| 170 | ? noVoters.filter((e) => e.characterNation === nation) |
| 171 | : []; |
| 172 | const lines = [ |
| 173 | ...yesEntries.map((e) => formatCharRow(e, false, hasRank)), |
| 174 | ...noEntries.map((e) => `❌ ${formatCharRow(e, false, hasRank)}`), |
| 175 | ]; |
| 176 | return lines.length > 0 ? lines.join("\n") : "—"; |
| 177 | }; |
| 178 | |
| 179 | const formatMessages = (): string => { |
| 180 | if (allMessages.length === 0) return ""; |
| 181 | return allMessages |
| 182 | .map((m) => { |
| 183 | const name = m.entry.characterName ?? m.entry.displayName; |
| 184 | const prefix = m.voteType === "no" ? "✗ " : "✓ "; |
| 185 | const msg = m.entry.publicMessage ? ` — ${m.entry.publicMessage}` : ""; |
| 186 | return `${prefix}${name} · ${m.entry.votedAt}${msg}`; |
| 187 | }) |
| 188 | .join("\n"); |
| 189 | }; |
| 190 | |
| 191 | const locked = state.locked; |
| 192 | const confirmed = state.confirmed; |
| 193 | |
| 194 | const color = |
| 195 | confirmed === "yes" ? 0x57f287 : |
| 196 | confirmed === "no" ? 0xed4245 : |
| 197 | locked ? 0x888888 : |
| 198 | 0xe8a317; |
| 199 | |
| 200 | // Title with nation + no counts (hidden when confirmed or locked) |
| 201 | const counts = !locked && confirmed === null |
| 202 | ? ` ${capellaEmoji} ${yesByNation.Capella.length} ${procyonEmoji} ${yesByNation.Procyon.length}` |
| 203 | : ""; |
| 204 | const statusSuffix = |
| 205 | locked ? " 🔒" : |
| 206 | confirmed === "yes" ? " ✅" : |
| 207 | confirmed === "no" ? " ❌" : ""; |
| 208 | |
| 209 | const title = `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`; |
| 210 | |
| 211 | const embed = new EmbedBuilder() |
| 212 | .setTitle(title) |
| 213 | .setColor(color) |
| 214 | .addFields( |
| 215 | { name: `${capellaEmoji} Capella (${yesByNation.Capella.length})`, value: formatNationField("Capella"), inline: false }, |
| 216 | { name: "\u200b", value: "\u200b", inline: false }, |
| 217 | { name: `${procyonEmoji} Procyon (${yesByNation.Procyon.length})`, value: formatNationField("Procyon"), inline: false }, |
| 218 | ) |
| 219 | .setTimestamp(); |
| 220 | |
| 221 | const msgSection = formatMessages(); |
| 222 | if (msgSection) { |
| 223 | embed.addFields({ name: "\u200b", value: msgSection, inline: false }); |
| 224 | } |
| 225 | |
| 226 | let footer: string; |
| 227 | if (confirmed === "yes") footer = cfg("confirmYesMessage"); |
| 228 | else if (confirmed === "no") footer = cfg("confirmNoMessage"); |
| 229 | else if (locked) footer = overrideLockMsg ?? cfg("lockMessage"); |
| 230 | else footer = `❌ ${noVoters.length} • Vote updates live • Anyone can vote • /tg switch to change character`; |
| 231 | embed.setFooter({ text: footer }); |
| 232 | |
| 233 | return embed; |
| 234 | } |
| 235 | |
| 236 | export function buildButtons( |
| 237 | disabled: boolean, |
| 238 | showSubmit?: boolean |
| 239 | ): ActionRowBuilder<ButtonBuilder>[] { |
| 240 | if (showSubmit) { |
| 241 | const scoreEmoji = getEmoji("score"); |
| 242 | const submitBtn = new ButtonBuilder() |
| 243 | .setCustomId("tg_score_submit") |
| 244 | .setLabel("Submit Score") |
| 245 | .setStyle(ButtonStyle.Secondary); |
| 246 | if (scoreEmoji) submitBtn.setEmoji(format.emoji(scoreEmoji) ?? scoreEmoji); |
| 247 | return [new ActionRowBuilder<ButtonBuilder>().addComponents(submitBtn)]; |
| 248 | } |
| 249 | |
| 250 | const yesBtn = new ButtonBuilder() |
| 251 | .setCustomId("tg_yes").setLabel("✅ Yes").setStyle(ButtonStyle.Success).setDisabled(disabled); |
| 252 | const noBtn = new ButtonBuilder() |
| 253 | .setCustomId("tg_no").setLabel("❌ No").setStyle(ButtonStyle.Danger).setDisabled(disabled); |
| 254 | return [new ActionRowBuilder<ButtonBuilder>().addComponents(yesBtn, noBtn)]; |
| 255 | } |
| 256 | |
| 257 | export async function updatePollMessage( |
| 258 | channel: TextChannel, |
| 259 | slot: number, |
| 260 | overrideLockMsg?: string, |
| 261 | showSubmit?: boolean |
| 262 | ): Promise<void> { |
| 263 | const state = polls.get(slot); |
| 264 | if (!state?.messageId) return; |
| 265 | console.log(`[updatePollMessage] slot=${slot} showSubmit=${showSubmit} messageId=${state.messageId}`); |
| 266 | const buttons = buildButtons(state.locked || state.confirmed !== null, showSubmit); |
| 267 | console.log(`[updatePollMessage] components rows=${buttons.length}`); |
| 268 | try { |
| 269 | const msg = await channel.messages.fetch(state.messageId); |
| 270 | await msg.edit({ embeds: [buildEmbed(state, overrideLockMsg)], components: buttons }); |
| 271 | } catch (err) { |
| 272 | console.error("Failed to update poll message:", err); |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | export async function postPoll(channel: TextChannel, slot: TGSlot): Promise<void> { |
| 277 | resetPollOverrides(); |
| 278 | persist.clear(); |
| 279 | |
| 280 | clearSessionBorrows(); |
| 281 | clearAllImpersonations(); |
| 282 | |
| 283 | const state: PollState = { |
| 284 | messageId: null, slot: slot.tgHour, |
| 285 | yes: new Map(), no: new Map(), |
| 286 | locked: false, confirmed: null, |
| 287 | }; |
| 288 | polls.set(slot.tgHour, state); |
| 289 | const msg = await channel.send({ embeds: [buildEmbed(state)], components: buildButtons(false) }); |
| 290 | state.messageId = msg.id; |
| 291 | console.log(`[${new Date().toISOString()}] Poll posted for ${slot.tgHour}:00.`); |
| 292 | |
| 293 | persist.save(polls) |
| 294 | } |
| 295 | |
| 296 | export function createVoteEntry( |
| 297 | userId: string, |
| 298 | member: GuildMember, |
| 299 | userKey: string | null, |
| 300 | discordUsername: string |
| 301 | ): Omit<VoteEntry, "votedAt" | "previousYesAt" | "previousNoAt" | "publicMessage"> { |
| 302 | const serverNickname = member.nickname ?? null; |
| 303 | const globalNickname = member.user.globalName ?? null; |
| 304 | const displayName = serverNickname ?? globalNickname ?? discordUsername; |
| 305 | |
| 306 | const { getEffectiveCharacter } = require("@systems/borrow"); |
| 307 | const { char, borrowedFrom: bf } = userKey |
| 308 | ? getEffectiveCharacter(userKey) |
| 309 | : { char: null, borrowedFrom: null }; |
| 310 | console.log(`[createVoteEntry] userKey=${userKey} char=${char?.name} borrowedFrom=${bf}`); |
| 311 | |
| 312 | return { |
| 313 | userKey: userKey ?? (undefined as any), |
| 314 | displayName, |
| 315 | characterName: char?.name, |
| 316 | characterClass: char?.class, |
| 317 | characterLevel: char?.level, |
| 318 | characterNation: char?.nation ?? (resolveNation(member, userKey) ?? undefined), |
| 319 | borrowedFrom: bf ?? undefined, |
| 320 | }; |
| 321 | } |