173 lines
No EOL
7.7 KiB
TypeScript
173 lines
No EOL
7.7 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<string, any>) => {
|
|
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<void> {
|
|
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);
|
|
} |