Zuletzt aktiv 1 month ago

interactions.ts Originalformat
1import { Interaction, ChatInputCommandInteraction, ButtonInteraction, TextChannel, StringSelectMenuInteraction } from "discord.js";
2import { handleButton, handleScoreSubmitButton } from "@handlers/buttons";
3import { handleTgCommand } from "@commands/tg";
4import { handleTgConfigCommand } from "@commands/tgConfig";
5import { handleBorrowAcceptButton } from "@subcommands/char/accept";
6import { handleBorrowDeclineButton } from "@subcommands/char/decline";
7import { handleConflictButton } from "@systems/conflict";
8import { handleImpersonateButton } from "@subcommands/impersonate";
9import { handleAutocomplete } from "@handlers/autocomplete";
10import { setActiveCharacter, getCharacterByName, getCharacters } from "@systems/characters";
11import { setPersistentPreference, clearSessionBorrowForUser, getEffectiveCharacter } from "@systems/borrow";
12import { polls, updatePollMessage } from "@systems/poll";
13import { cfg } from "@systems/config";
14import { resolveMessage, nowFormatted } from "@systems/messages";
15import { format } from "@format";
16import { modals } from "@handlers/modals";
17import fs from "fs";
18import path from "path";
19
20async function handleSwitchAfterReclaim(btn: ButtonInteraction): Promise<void> {
21 const parts = btn.customId.split(":");
22 const userKey = parts[1];
23 const charName = parts[2];
24 const prevVoteType = (parts[3] ?? "yes") as "yes" | "no";
25
26 const chars = JSON.parse(
27 fs.readFileSync(path.join(__dirname, "../../data/characters.json"), "utf8")
28 );
29
30 let resolvedChar: any = null;
31 let borrowedFrom: string | null = null;
32
33 // Try own char first
34 const ownEntry = chars[userKey]?.characters?.find((c: any) => c.name === charName);
35 if (ownEntry) {
36 setActiveCharacter(userKey, charName);
37 clearSessionBorrowForUser(userKey);
38 resolvedChar = ownEntry;
39 } else {
40 // Try shared char
41 for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) {
42 const char = data.characters?.find(
43 (c: any) => c.name === charName && c.sharedWith?.includes(userKey)
44 );
45 if (char) {
46 setPersistentPreference(userKey, ownerKey, charName);
47 clearSessionBorrowForUser(userKey);
48 resolvedChar = char;
49 borrowedFrom = ownerKey;
50 break;
51 }
52 }
53 }
54
55 if (!resolvedChar) {
56 await btn.reply({ content: `❌ Could not switch to **${charName}**.`, ephemeral: true });
57 return;
58 }
59
60 // Re-add to poll with previous vote type
61 const slot = [...polls.keys()][0];
62 const state = slot !== undefined ? polls.get(slot) : null;
63
64 if (state && !state.locked && state.confirmed === null) {
65 const { char } = getEffectiveCharacter(userKey);
66 const now = nowFormatted();
67 const publicMsg = resolveMessage("public", prevVoteType, 1, userKey, null, null);
68 const voteEntry = {
69 userKey,
70 displayName: charName,
71 characterName: char?.name ?? charName,
72 characterClass: char?.class ?? resolvedChar.class,
73 characterLevel: char?.level ?? resolvedChar.level,
74 characterNation: char?.nation ?? resolvedChar.nation,
75 borrowedFrom: borrowedFrom ?? undefined,
76 discordId: btn.user.id,
77 votedAt: now,
78 publicMessage: publicMsg ?? undefined,
79 };
80
81 for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) {
82 if (entry.userKey === userKey) {
83 state.yes.delete(id);
84 state.no.delete(id);
85 }
86 }
87
88 if (prevVoteType === "yes") {
89 state.yes.set(`switch_reclaim:${userKey}`, voteEntry);
90 } else {
91 state.no.set(`switch_reclaim:${userKey}`, voteEntry);
92 }
93
94 console.log(`[switch_reclaim] cleaning up for userKey=${userKey}`);
95 console.log(`[switch_reclaim] yes keys:`, [...state.yes.entries()].map(([id, e]) => `${id}:${e.userKey}`));
96 console.log(`[switch_reclaim] no keys:`, [...state.no.entries()].map(([id, e]) => `${id}:${e.userKey}`));
97
98 const channel = await btn.client.channels.fetch(cfg("pollChannelId")) as TextChannel;
99 await updatePollMessage(channel, slot!);
100 }
101
102 const charDisplay = resolvedChar ? format.char(resolvedChar) : charName;
103 const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : "";
104 await btn.reply({
105 content: `🔄 ${charDisplay}${borrowNote}${state ? ` — re-added to poll as **${prevVoteType}**.` : ""}`,
106 ephemeral: true,
107 });
108}
109
110export async function handleInteraction(interaction: Interaction): Promise<void> {
111 try {
112 if (interaction.isAutocomplete()) {
113 await handleAutocomplete(interaction);
114 return;
115 }
116
117 if (interaction.isButton()) {
118 const btn = interaction as ButtonInteraction;
119
120 console.log("[interactions] interaction btnId:", btn.customId);
121 if (btn.customId.startsWith("conflict_")) {
122 console.log("[interactions] routing to conflict handler:", btn.customId);
123 return await handleConflictButton(btn);
124 }
125
126 if (btn.customId.startsWith("impersonate_")) {
127 return await handleImpersonateButton(btn);
128 }
129
130 if (btn.customId.startsWith("switch_after_reclaim:")) {
131 return await handleSwitchAfterReclaim(btn);
132 }
133
134 if (btn.customId.startsWith("borrow_accept:")) {
135 const [, ownerKey, requesterKey] = btn.customId.split(":");
136 return await handleBorrowAcceptButton(btn, ownerKey, requesterKey);
137 }
138
139 if (btn.customId.startsWith("borrow_decline:")) {
140 const [, ownerKey, requesterKey] = btn.customId.split(":");
141 return await handleBorrowDeclineButton(btn, ownerKey, requesterKey);
142 }
143
144 if (btn.customId === "tg_score_submit") {
145 return await handleScoreSubmitButton(btn);
146 }
147
148 return await handleButton(btn);
149 }
150
151 if (interaction.isModalSubmit()) {
152 return await modals.handleModal(interaction);
153 }
154
155 if (interaction.isStringSelectMenu()) {
156 const sel = interaction as StringSelectMenuInteraction;
157 if (sel.customId.startsWith("score_slot_select:")) {
158 const userKey = sel.customId.split(":")[1];
159 const slot = parseInt(sel.values[0], 10);
160 await sel.showModal(modals.buildScoreModal(userKey, slot));
161 return;
162 }
163 }
164
165 if (interaction.isChatInputCommand()) {
166 const cmd = interaction as ChatInputCommandInteraction;
167 if (cmd.commandName === "tg") await handleTgCommand(cmd);
168 if (cmd.commandName === "tg-config") await handleTgConfigCommand(cmd);
169 }
170 } catch (err) {
171 console.error("Interaction error:", err);
172 try {
173 const msg = { content: "❌ An error occurred.", ephemeral: true };
174 if ((interaction as any).replied || (interaction as any).deferred) {
175 await (interaction as any).followUp(msg);
176 } else {
177 await (interaction as any).reply(msg);
178 }
179 } catch {}
180 }
181}