import { ChatInputCommandInteraction } from "discord.js"; import { Config } from "@systems/config"; import { hasOfficerRole } from "@systems/users"; import { getUsermapEntryById, setUsermapEntry, removeUsermapEntry } from "@systems/messages"; import { replyAndDelete } from "@utils"; import { Nations } from "@systems/nations"; import { polls, updatePollMessage } from "@systems/poll"; import { getEffectiveCharacter } from "@systems/borrow"; export async function handleAdminUserMap(interaction: ChatInputCommandInteraction): Promise { const member = await interaction.guild!.members.fetch(interaction.user.id); if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) { return void replyAndDelete(interaction, "❌ You don't have permission to use this command."); } const sub = interaction.options.getSubcommand(); // ── map ────────────────────────────────────────────────────────────────────── if (sub === "map") { const target = interaction.options.getUser("user", true); const userKey = interaction.options.getString("userkey", true); // Check if userKey exists in characters.json const fs = require("fs"); const path = require("path"); const chars = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../data/characters.json"), "utf8")); if (!chars[userKey]) { return void replyAndDelete(interaction, `❌ No characters found for userKey **${userKey}**. Make sure it exists in characters.json first.`, true); } // Check if this Discord ID is already mapped const existing = getUsermapEntryById(target.id, target.username); const entry = existing ?? { file: userKey, aliases: [target.globalName ?? target.username] }; entry.file = userKey; setUsermapEntry(target.id, entry); return void replyAndDelete(interaction, `✅ Mapped **${target.username}** (${target.id}) → **${userKey}**`, true); } // ── unmap ──────────────────────────────────────────────────────────────────── if (sub === "unmap") { const target = interaction.options.getUser("user", true); const entry = getUsermapEntryById(target.id, target.username); if (!entry) { return void replyAndDelete(interaction, `❌ No mapping found for **${target.username}**.`, true); } removeUsermapEntry(target.id); // Also remove old username-based entry if it exists removeUsermapEntry(target.username); return void replyAndDelete(interaction, `✅ Removed mapping for **${target.username}** (was → **${entry.file}**)`, true); } // ── list ───────────────────────────────────────────────────────────────────── if (sub === "list") { const fs = require("fs"); const path = require("path"); const raw = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../data/usermap.json"), "utf8")); const lines: string[] = []; for (const [key, value] of Object.entries(raw)) { const file = typeof value === "string" ? value : (value as any).file; const aliases = typeof value === "object" ? ((value as any).aliases ?? []) : []; const isId = /^\d{17,20}$/.test(key); const type = isId ? "🆔" : "👤"; lines.push(`${type} \`${key}\` → **${file}**${aliases.length ? ` *(${aliases.join(", ")})*` : ""}`); } if (lines.length === 0) return void replyAndDelete(interaction, "No usermap entries found.", true); // Split into chunks to avoid Discord's 2000 char limit const chunks: string[] = []; let current = ""; for (const line of lines) { if (current.length + line.length + 1 > 1900) { chunks.push(current); current = line; } else { current += (current ? "\n" : "") + line; } } if (current) chunks.push(current); await interaction.reply({ content: chunks[0], ephemeral: true }); for (const chunk of chunks.slice(1)) { await interaction.followUp({ content: chunk, ephemeral: true }); } return; } } export async function handleAdminPollFixVoter(interaction: ChatInputCommandInteraction): Promise { const member = await interaction.guild!.members.fetch(interaction.user.id); if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) { return void replyAndDelete(interaction, "❌ You don't have permission to use this command."); } const target = interaction.options.getUser("user", true); const targetMember = await interaction.guild!.members.fetch(target.id); const entry = getUsermapEntryById(target.id, target.username); if (!entry) { return void replyAndDelete(interaction, `❌ **${target.username}** is not registered in usermap.json. Use \`/tg-admin user map\` first.`, true); } const slot = [...polls.keys()][0]; if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true); const state = polls.get(slot)!; // Find voter entry by discord ID or by userKey let foundId: string | null = null; for (const [id, e] of [...state.yes.entries(), ...state.no.entries()]) { if ((e as any).discordId === target.id || e.userKey === entry.file) { foundId = id; break; } } if (!foundId) return void replyAndDelete(interaction, `❌ **${target.username}** has not voted in the active poll.`, true); const { char, borrowedFrom } = getEffectiveCharacter(entry.file); if (!char) return void replyAndDelete(interaction, `❌ No active character found for **${entry.file}**.`, true); const nation = Nations.resolve(targetMember, entry.file); const updateEntry = (map: Map) => { const e = map.get(foundId!); if (e) { e.userKey = entry.file; e.characterName = char.name; e.characterClass = char.class; e.characterLevel = char.level; e.characterNation = char.nation ?? nation; e.borrowedFrom = borrowedFrom ?? undefined; e.discordId = target.id; } }; updateEntry(state.yes); updateEntry(state.no); const channel = await interaction.client.channels.fetch(Config.get({ section: "channels", key: "poll" })) as any; await updatePollMessage(channel, slot); const { format } = require("@format"); return void replyAndDelete(interaction, `✅ Fixed poll entry for **${target.username}** → ${format.char(char)}`, true); } export async function handleAdminPollShowEntry(interaction: ChatInputCommandInteraction): Promise { const member = await interaction.guild!.members.fetch(interaction.user.id); if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) { return void replyAndDelete(interaction, "❌ You don't have permission to use this command."); } const target = interaction.options.getUser("user", true); const { polls } = require("@systems/poll"); const slot = [...polls.keys()][0]; if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true); const state = polls.get(slot)!; let found: any = null; for (const [id, e] of [...state.yes.entries(), ...state.no.entries()]) { if ((e as any).discordId === target.id || e.userKey === target.id) { found = { voteId: id, ...e }; break; } } if (!found) return void replyAndDelete(interaction, `❌ No poll entry found for **${target.username}**.`, true); return void replyAndDelete(interaction, `\`\`\`json\n${JSON.stringify(found, null, 2)}\n\`\`\``, true); }