import { Interaction, ChatInputCommandInteraction, ButtonInteraction, TextChannel, StringSelectMenuInteraction } from "discord.js"; import { handleButton, handleScoreSubmitButton } from "@handlers/buttons"; import { handleTgCommand } from "@commands/tg"; import { handleTgConfigCommand } from "@commands/tgConfig"; import { handleBorrowAcceptButton } from "@subcommands/char/accept"; import { handleBorrowDeclineButton } from "@subcommands/char/decline"; import { handleConflictButton } from "@systems/conflict"; import { handleImpersonateButton } from "@subcommands/impersonate"; import { handleAutocomplete } from "@handlers/autocomplete"; import { setActiveCharacter, getCharacterByName, getCharacters } from "@systems/characters"; import { setPersistentPreference, clearSessionBorrowForUser, getEffectiveCharacter } from "@systems/borrow"; import { polls, updatePollMessage } from "@systems/poll"; import { cfg } from "@systems/config"; import { resolveMessage, nowFormatted } from "@systems/messages"; import { format } from "@format"; import { modals } from "@handlers/modals"; import fs from "fs"; import path from "path"; async function handleSwitchAfterReclaim(btn: ButtonInteraction): Promise { const parts = btn.customId.split(":"); const userKey = parts[1]; const charName = parts[2]; const prevVoteType = (parts[3] ?? "yes") as "yes" | "no"; const chars = JSON.parse( fs.readFileSync(path.join(__dirname, "../../data/characters.json"), "utf8") ); let resolvedChar: any = null; let borrowedFrom: string | null = null; // Try own char first const ownEntry = chars[userKey]?.characters?.find((c: any) => c.name === charName); if (ownEntry) { setActiveCharacter(userKey, charName); clearSessionBorrowForUser(userKey); resolvedChar = ownEntry; } else { // Try shared char for (const [ownerKey, data] of Object.entries(chars) as [string, any][]) { const char = data.characters?.find( (c: any) => c.name === charName && c.sharedWith?.includes(userKey) ); if (char) { setPersistentPreference(userKey, ownerKey, charName); clearSessionBorrowForUser(userKey); resolvedChar = char; borrowedFrom = ownerKey; break; } } } if (!resolvedChar) { await btn.reply({ content: `❌ Could not switch to **${charName}**.`, ephemeral: true }); return; } // Re-add to poll with previous vote type const slot = [...polls.keys()][0]; const state = slot !== undefined ? polls.get(slot) : null; if (state && !state.locked && state.confirmed === null) { const { char } = getEffectiveCharacter(userKey); const now = nowFormatted(); const publicMsg = resolveMessage("public", prevVoteType, 1, userKey, null, null); const voteEntry = { userKey, displayName: charName, characterName: char?.name ?? charName, characterClass: char?.class ?? resolvedChar.class, characterLevel: char?.level ?? resolvedChar.level, characterNation: char?.nation ?? resolvedChar.nation, borrowedFrom: borrowedFrom ?? undefined, discordId: btn.user.id, votedAt: now, publicMessage: publicMsg ?? undefined, }; for (const [id, entry] of [...state.yes.entries(), ...state.no.entries()]) { if (entry.userKey === userKey) { state.yes.delete(id); state.no.delete(id); } } if (prevVoteType === "yes") { state.yes.set(`switch_reclaim:${userKey}`, voteEntry); } else { state.no.set(`switch_reclaim:${userKey}`, voteEntry); } console.log(`[switch_reclaim] cleaning up for userKey=${userKey}`); console.log(`[switch_reclaim] yes keys:`, [...state.yes.entries()].map(([id, e]) => `${id}:${e.userKey}`)); console.log(`[switch_reclaim] no keys:`, [...state.no.entries()].map(([id, e]) => `${id}:${e.userKey}`)); const channel = await btn.client.channels.fetch(cfg("pollChannelId")) as TextChannel; await updatePollMessage(channel, slot!); } const charDisplay = resolvedChar ? format.char(resolvedChar) : charName; const borrowNote = borrowedFrom ? ` *(shared by ${borrowedFrom})*` : ""; await btn.reply({ content: `🔄 ${charDisplay}${borrowNote}${state ? ` — re-added to poll as **${prevVoteType}**.` : ""}`, ephemeral: true, }); } export async function handleInteraction(interaction: Interaction): Promise { try { if (interaction.isAutocomplete()) { await handleAutocomplete(interaction); return; } if (interaction.isButton()) { const btn = interaction as ButtonInteraction; console.log("[interactions] interaction btnId:", btn.customId); if (btn.customId.startsWith("conflict_")) { console.log("[interactions] routing to conflict handler:", btn.customId); return await handleConflictButton(btn); } if (btn.customId.startsWith("impersonate_")) { return await handleImpersonateButton(btn); } if (btn.customId.startsWith("switch_after_reclaim:")) { return await handleSwitchAfterReclaim(btn); } if (btn.customId.startsWith("borrow_accept:")) { const [, ownerKey, requesterKey] = btn.customId.split(":"); return await handleBorrowAcceptButton(btn, ownerKey, requesterKey); } if (btn.customId.startsWith("borrow_decline:")) { const [, ownerKey, requesterKey] = btn.customId.split(":"); return await handleBorrowDeclineButton(btn, ownerKey, requesterKey); } if (btn.customId === "tg_score_submit") { return await handleScoreSubmitButton(btn); } return await handleButton(btn); } if (interaction.isModalSubmit()) { return await modals.handleModal(interaction); } if (interaction.isStringSelectMenu()) { const sel = interaction as StringSelectMenuInteraction; if (sel.customId.startsWith("score_slot_select:")) { const userKey = sel.customId.split(":")[1]; const slot = parseInt(sel.values[0], 10); await sel.showModal(modals.buildScoreModal(userKey, slot)); return; } } if (interaction.isChatInputCommand()) { const cmd = interaction as ChatInputCommandInteraction; if (cmd.commandName === "tg") await handleTgCommand(cmd); if (cmd.commandName === "tg-config") await handleTgConfigCommand(cmd); } } catch (err) { console.error("Interaction error:", err); try { const msg = { content: "❌ An error occurred.", ephemeral: true }; if ((interaction as any).replied || (interaction as any).deferred) { await (interaction as any).followUp(msg); } else { await (interaction as any).reply(msg); } } catch {} } }