160 lines
No EOL
6.1 KiB
TypeScript
160 lines
No EOL
6.1 KiB
TypeScript
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<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)
|
|
Attendance.snapshot(slot, state.lockedYesKeys);
|
|
}
|
|
|
|
// ─── Embed building ───────────────────────────────────────────────────────────
|
|
|
|
export function buildButtons(
|
|
disabled: boolean,
|
|
showSubmit?: boolean
|
|
): ActionRowBuilder<ButtonBuilder>[] {
|
|
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<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: [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<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: [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<VoteEntry, "votedAt" | "previousYesAt" | "previousNoAt" | "publicMessage"> {
|
|
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,
|
|
};
|
|
} |