import { ButtonInteraction, TextChannel } from "discord.js"; import { cfg } from "../systems/config"; import { pollReplyAndDelete } from "../utils"; import { resolveUser } from "../systems/users"; import { resolveMessage, nowFormatted } from "../systems/messages"; import { resolveNation } from "../systems/nations"; import { polls, updatePollMessage, createVoteEntry, getPublicOverride, getEphemeralOverride } from "../systems/poll"; import { showConflictEmbed, handleConflictButton } from "../systems/conflict"; import { getCharacters } from "../systems/characters"; const LOCK_AT = parseInt(process.env.LOCK_AT ?? "10"); const clickCounts = new Map(); export async function handleButton(interaction: ButtonInteraction): Promise { if (!["tg_yes", "tg_no"].includes(interaction.customId)) return; // Defer immediately to avoid 3s timeout await interaction.deferUpdate(); const slot = [...polls.entries()].find(([, s]) => s.messageId === interaction.message.id)?.[0]; if (slot === undefined) return; const state = polls.get(slot)!; if (state.locked || state.confirmed !== null) return; const userId = interaction.user.id; const member = interaction.guild!.members.cache.get(userId) ?? await interaction.guild!.members.fetch(userId); const user = await resolveUser(member); const votedYes = interaction.customId === "tg_yes"; const now = nowFormatted(); // Check nation — block if no nation const nation = resolveNation(member, user.userKey); if (!nation) { await pollReplyAndDelete(interaction, "❌ You must be in Capella or Procyon to vote."); return; } // Click tracking if (!clickCounts.has(userId)) clickCounts.set(userId, { yes: 0, no: 0 }); const clicks = clickCounts.get(userId)!; if (votedYes && clicks.yes >= LOCK_AT) return; if (!votedYes && clicks.no >= LOCK_AT) return; // Ignore same vote if (votedYes && state.yes.has(userId)) return; if (!votedYes && state.no.has(userId)) return; if (votedYes) clicks.yes += 1; else clicks.no += 1; const clickCount = votedYes ? clicks.yes : clicks.no; // Resolve messages — officer override takes priority const publicMsg = getPublicOverride(userId, votedYes ? "yes" : "no") ?? resolveMessage("public", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname); const ephemeralMsg = getEphemeralOverride(userId, votedYes ? "yes" : "no") ?? resolveMessage("ephemeral", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname); const baseEntry = createVoteEntry(userId, member, user.userKey, user.discordUsername); // Check if character is already in use by another voter if (baseEntry.characterName) { for (const [otherId, entry] of [...state.yes.entries(), ...state.no.entries()]) { if (otherId !== userId && entry.characterName === baseEntry.characterName) { // Check if this user is the owner of the character const borrowerKey = entry.borrowedFrom ? entry.userKey : null; const ownerKey = entry.borrowedFrom ?? entry.userKey; const isOwner = user.userKey === ownerKey; if (isOwner && baseEntry.userKey) { // Owner trying to reclaim — show conflict embed const allChars = getCharacters(baseEntry.userKey); const borrowedChar = allChars.find((c) => c.name === baseEntry.characterName); if (borrowedChar) { await showConflictEmbed(interaction, baseEntry.userKey, entry.userKey, borrowedChar, allChars); return; } } await pollReplyAndDelete(interaction, `❌ **${baseEntry.characterName}** is already in the poll by another player.` ); return; } } } if (votedYes) { const previousNo = state.no.get(userId); state.no.delete(userId); state.yes.set(userId, { ...baseEntry, votedAt: now, previousNoAt: previousNo?.votedAt, publicMessage: publicMsg ?? undefined, }); } else { const previousYes = state.yes.get(userId); state.yes.delete(userId); state.no.set(userId, { ...baseEntry, votedAt: now, previousYesAt: previousYes?.votedAt, publicMessage: publicMsg ?? undefined, }); } const locked = clickCount >= LOCK_AT; if (locked) state.locked = true; // Send poll ephemeral follow-up const lockedSuffix = locked ? "\n🔒 *You've been locked in.*" : ""; const msgContent = ephemeralMsg ? `${ephemeralMsg}${lockedSuffix}` : locked ? "🔒 You've been locked in." : null; await pollReplyAndDelete(interaction, msgContent); const channel = interaction.channel as TextChannel; await updatePollMessage(channel, slot); } export function resetClickCounts(): void { clickCounts.clear(); }