import { EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, TextChannel, GuildMember, } from "discord.js"; import { PollState, VoteEntry, Nation, TGSlot } from "@src/types"; import { Emoji } from "@systems/emojis"; import { Nations } from "@systems/nations"; import { format } from "@format"; import { persist } from "@systems/pollPersistence" import { clearSessionBorrows, getEffectiveCharacter } from "@systems/borrow"; import { clearAllImpersonations } from "@systems/impersonate"; import { Attendance } from "@systems/attendance"; import { PollUI } from "@ui/poll"; // ─── Poll state ─────────────────────────────────────────────────────────────── export const polls: Map = new Map(); const publicOverrides: Map = new Map(); const ephemeralOverrides: Map = 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) Attendance.snapshot(slot, state.lockedYesKeys); } // ─── Embed building ─────────────────────────────────────────────────────────── export function buildButtons( disabled: boolean, showSubmit?: boolean ): ActionRowBuilder[] { if (showSubmit) { const scoreEmoji = Emoji.get("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().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().addComponents(yesBtn, noBtn)]; } export async function updatePollMessage( channel: TextChannel, slot: number, overrideLockMsg?: string, showSubmit?: boolean ): Promise { 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: [PollUI.buildEmbed(state, { overrideLockMsg })], components: buttons }); } catch (err) { console.error("Failed to update poll message:", err); } } export async function postPoll(channel: TextChannel, slot: TGSlot): Promise { 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: [PollUI.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 { const serverNickname = member.nickname ?? null; const globalNickname = member.user.globalName ?? null; const displayName = serverNickname ?? globalNickname ?? discordUsername; 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 ?? (Nations.resolve(member, userKey) ?? undefined), borrowedFrom: bf ?? undefined, }; }