Zuletzt aktiv 1 month ago

Änderung e9dcd099946ee0578fac87a3dba40f915c6f2a7f

buttons.ts Originalformat
1import { ButtonInteraction, TextChannel } from "discord.js";
2import { cfg } from "../systems/config";
3import { pollReplyAndDelete } from "../utils";
4import { resolveUser } from "../systems/users";
5import { resolveMessage, nowFormatted } from "../systems/messages";
6import { resolveNation } from "../systems/nations";
7import { polls, updatePollMessage, createVoteEntry, getPublicOverride, getEphemeralOverride } from "../systems/poll";
8import { showConflictEmbed, handleConflictButton } from "../systems/conflict";
9import { getCharacters } from "../systems/characters";
10
11const LOCK_AT = parseInt(process.env.LOCK_AT ?? "10");
12
13const clickCounts = new Map<string, { yes: number; no: number }>();
14
15export async function handleButton(interaction: ButtonInteraction): Promise<void> {
16 if (!["tg_yes", "tg_no"].includes(interaction.customId)) return;
17
18 // Defer immediately to avoid 3s timeout
19 await interaction.deferUpdate();
20
21 const slot = [...polls.entries()].find(([, s]) => s.messageId === interaction.message.id)?.[0];
22 if (slot === undefined) return;
23
24 const state = polls.get(slot)!;
25 if (state.locked || state.confirmed !== null) return;
26
27 const userId = interaction.user.id;
28 const member = interaction.guild!.members.cache.get(userId)
29 ?? await interaction.guild!.members.fetch(userId);
30 const user = await resolveUser(member);
31 const votedYes = interaction.customId === "tg_yes";
32 const now = nowFormatted();
33
34 // Check nation — block if no nation
35 const nation = resolveNation(member, user.userKey);
36 if (!nation) {
37 await pollReplyAndDelete(interaction, "❌ You must be in Capella or Procyon to vote.");
38 return;
39 }
40
41 // Click tracking
42 if (!clickCounts.has(userId)) clickCounts.set(userId, { yes: 0, no: 0 });
43 const clicks = clickCounts.get(userId)!;
44
45 if (votedYes && clicks.yes >= LOCK_AT) return;
46 if (!votedYes && clicks.no >= LOCK_AT) return;
47
48 // Ignore same vote
49 if (votedYes && state.yes.has(userId)) return;
50 if (!votedYes && state.no.has(userId)) return;
51
52 if (votedYes) clicks.yes += 1;
53 else clicks.no += 1;
54
55 const clickCount = votedYes ? clicks.yes : clicks.no;
56
57 // Resolve messages — officer override takes priority
58 const publicMsg = getPublicOverride(userId, votedYes ? "yes" : "no")
59 ?? resolveMessage("public", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname);
60
61 const ephemeralMsg = getEphemeralOverride(userId, votedYes ? "yes" : "no")
62 ?? resolveMessage("ephemeral", votedYes ? "yes" : "no", clickCount, user.discordUsername, user.serverNickname, user.globalNickname);
63
64 const baseEntry = createVoteEntry(userId, member, user.userKey, user.discordUsername);
65
66 // Check if character is already in use by another voter
67 if (baseEntry.characterName) {
68 for (const [otherId, entry] of [...state.yes.entries(), ...state.no.entries()]) {
69 if (otherId !== userId && entry.characterName === baseEntry.characterName) {
70 // Check if this user is the owner of the character
71 const borrowerKey = entry.borrowedFrom ? entry.userKey : null;
72 const ownerKey = entry.borrowedFrom ?? entry.userKey;
73 const isOwner = user.userKey === ownerKey;
74
75 if (isOwner && baseEntry.userKey) {
76 // Owner trying to reclaim — show conflict embed
77 const allChars = getCharacters(baseEntry.userKey);
78 const borrowedChar = allChars.find((c) => c.name === baseEntry.characterName);
79 if (borrowedChar) {
80 await showConflictEmbed(interaction, baseEntry.userKey, entry.userKey, borrowedChar, allChars);
81 return;
82 }
83 }
84
85 await pollReplyAndDelete(interaction,
86 `❌ **${baseEntry.characterName}** is already in the poll by another player.`
87 );
88 return;
89 }
90 }
91 }
92
93 if (votedYes) {
94 const previousNo = state.no.get(userId);
95 state.no.delete(userId);
96 state.yes.set(userId, {
97 ...baseEntry,
98 votedAt: now,
99 previousNoAt: previousNo?.votedAt,
100 publicMessage: publicMsg ?? undefined,
101 });
102 } else {
103 const previousYes = state.yes.get(userId);
104 state.yes.delete(userId);
105 state.no.set(userId, {
106 ...baseEntry,
107 votedAt: now,
108 previousYesAt: previousYes?.votedAt,
109 publicMessage: publicMsg ?? undefined,
110 });
111 }
112
113 const locked = clickCount >= LOCK_AT;
114 if (locked) state.locked = true;
115
116 // Send poll ephemeral follow-up
117 const lockedSuffix = locked ? "\n🔒 *You've been locked in.*" : "";
118 const msgContent = ephemeralMsg
119 ? `${ephemeralMsg}${lockedSuffix}`
120 : locked ? "🔒 You've been locked in." : null;
121 await pollReplyAndDelete(interaction, msgContent);
122
123 const channel = interaction.channel as TextChannel;
124 await updatePollMessage(channel, slot);
125}
126
127export function resetClickCounts(): void {
128 clickCounts.clear();
129}