Ostatnio aktywny 1 month ago

nuno zrewidował ten Gist 1 month ago. Przejdź do rewizji

1 file changed, 128 insertions

buttons.ts(stworzono plik)

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