import { SlashCommandBuilder } from "discord.js"; import type { AutocompleteInteraction, ChatInputCommandInteraction, Client, GuildMember, } from "discord.js"; import { ChannelType, type GuildChannel, type Role } from "discord.js"; import cfg from "@/config"; import { db, guild as guildTable, manager as managerTable } from "@/database"; import { getGuild } from "@/database/queries"; import { hasManagerPermissions } from "@/utils/discord/permissions"; import { loggers } from "@/utils/logging"; import { eq } from "drizzle-orm"; const logger = loggers.interactions.child({ name: "configCommand" }); const CONFIG_KEYS = [ "suggestion_forum_id", "bug_channel_id", "highlights_channel_id", "commands_channel_id", "ticket_channel_id", "ticket_category_id", "ticket_message", ] as const; function isChannelKey(key: string) { return key.endsWith("_id"); } const commandData = new SlashCommandBuilder() .setName("config") .setDescription("configure bot settings for this server") .addSubcommand((sub) => sub .setName("manager-role") .setDescription("manage roles that can manage reports/tickets") .addStringOption((opt) => opt .setName("action") .setDescription("action to perform") .setRequired(true) .addChoices( { name: "add", value: "add" }, { name: "remove", value: "remove" }, { name: "list", value: "list" }, ), ) .addRoleOption((opt) => opt.setName("role").setDescription("role to add/remove"), ), ) .addSubcommand((sub) => sub .setName("set") .setDescription("set a guild configuration key") .addStringOption((opt) => opt .setName("key") .setDescription("configuration key to set") .setRequired(true) .setAutocomplete(true), ) .addChannelOption((opt) => opt .setName("channel") .setDescription("channel to set (for channel keys)") .setRequired(false), ) .addStringOption((opt) => opt .setName("value") .setDescription("value to set (for non-channel keys)") .setRequired(false), ), ) .addSubcommand((sub) => sub.setName("view").setDescription("view current server configuration"), ); async function autocomplete(interaction: AutocompleteInteraction) { const focused = interaction.options.getFocused(true); if (focused.name === "key") { const input = String(focused.value).toLowerCase(); const suggestions = CONFIG_KEYS.filter((k) => k.toLowerCase().includes(input), ).slice(0, 25); await interaction.respond(suggestions.map((s) => ({ name: s, value: s }))); } } async function execute( _client: Client, interaction: ChatInputCommandInteraction, ) { if (!interaction.guild || !interaction.member) { await interaction.reply({ content: "❌ this command can only be used in a server.", flags: ["Ephemeral"], }); return; } const isDev = interaction.user.id === cfg.developerId; const isManager = hasManagerPermissions(interaction.member as GuildMember); if (!(isDev || isManager)) { await interaction.reply({ content: "❌ You do not have permission to manage this server.", flags: ["Ephemeral"], }); return; } const sub = interaction.options.getSubcommand(); try { if (sub === "manager-role") { await handleManagerRole(interaction); } else if (sub === "set") { await handleSet(interaction); } else if (sub === "view") { await handleView(interaction); } else { await interaction.reply({ content: "❌ Unknown subcommand.", flags: ["Ephemeral"], }); } } catch (err) { logger.error(err, "Failed to execute config command"); await interaction.reply({ content: "❌ Failed to update configuration, please try again later.", flags: ["Ephemeral"], }); } } async function handleManagerRole(interaction: ChatInputCommandInteraction) { if (!interaction.guild || !interaction.member) { await interaction.reply({ content: "❌ This command can only be used in a server.", flags: ["Ephemeral"], }); return; } const action = interaction.options.getString("action", true); const role = interaction.options.getRole("role") as Role | null; const guildId = interaction.guild.id; const isDeveloper = interaction.user.id === cfg.developerId; const hasPerms = await hasManagerPermissions( interaction.member as GuildMember, ); if (!isDeveloper && !hasPerms) { await interaction.reply({ content: "❌ You do not have manager permissions to run this action.", flags: ["Ephemeral"], }); return; } if (action === "list") { const rows = await db .select({ roleId: managerTable.roleId }) .from(managerTable) .where(eq(managerTable.guildId, guildId)) .execute(); if (!rows || rows.length === 0) { await interaction.reply({ content: "📋 There are no manager roles configured for this server.", flags: ["Ephemeral"], }); return; } const mentions = rows.map((r) => `\* <@&${r.roleId}>`).join("\n"); await interaction.reply({ content: `**📋 Manager roles**\n${mentions}`, flags: ["Ephemeral"], }); return; } if (!role) { await interaction.reply({ content: "❌ You must specify a role for this action.", flags: ["Ephemeral"], }); return; } if (action === "add") { const existing = await db .select() .from(managerTable) .where(eq(managerTable.roleId, role.id)) .execute(); if (existing && existing.length > 0) { await interaction.reply({ content: `❌ ${role} is already configured as a manager role.`, flags: ["Ephemeral"], }); return; } await db .insert(managerTable) .values({ roleId: role.id, guildId, }) .execute(); await interaction.reply({ content: `✅ Added ${role} as a manager role.`, flags: ["Ephemeral"], }); return; } if (action === "remove") { await db .delete(managerTable) .where(eq(managerTable.roleId, role.id)) .execute(); await interaction.reply({ content: `✅ Removed ${role} from manager roles.`, flags: ["Ephemeral"], }); return; } await interaction.reply({ content: "❌ unknown action.", flags: ["Ephemeral"], }); } async function handleSet(interaction: ChatInputCommandInteraction) { if (!interaction.guild) { await interaction.reply({ content: "❌ This command can only be used in a server.", flags: ["Ephemeral"], }); return; } const key = interaction.options.getString("key", true); // @ts-expect-error shush if (!CONFIG_KEYS.includes(key)) { await interaction.reply({ content: "❌ Unknown configuration key.", flags: ["Ephemeral"], }); return; } const guildId = interaction.guild.id; await getGuild(guildId); if (isChannelKey(key)) { const channel = interaction.options.getChannel( "channel", false, ) as GuildChannel | null; if (!channel) { await interaction.reply({ content: "❌ this key expects a channel - please provide a channel.", flags: ["Ephemeral"], }); return; } const allowedTypes = [ ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.GuildCategory, ]; if (!allowedTypes.includes(channel.type)) { await interaction.reply({ content: "❌ Channel must be a text channel or category.", flags: ["Ephemeral"], }); return; } await db .update(guildTable) .set({ [key]: channel.id }) .where(eq(guildTable.guildId, guildId)) .execute(); await interaction.reply({ content: `✅ Set \`${key}\` to <#${channel.id}>.`, flags: ["Ephemeral"], }); return; } const value = interaction.options.getString("value", true); await db .update(guildTable) .set({ [key]: value }) .where(eq(guildTable.guildId, guildId)) .execute(); await interaction.reply({ content: `✅ set ${key} to \`${value}\`.`, flags: ["Ephemeral"], }); } async function handleView(interaction: ChatInputCommandInteraction) { if (!interaction.guild) { await interaction.reply({ content: "❌ This command can only be used in a server.", flags: ["Ephemeral"], }); return; } const guildId = interaction.guild.id; const result = await db .select() .from(guildTable) .where(eq(guildTable.guildId, guildId)) .execute(); if (!result || result.length === 0) { await interaction.reply({ content: "📋 No configuration found for this server.", flags: ["Ephemeral"], }); return; } const row = result[0] as Record; const lines = CONFIG_KEYS.map((k) => { const val = row[k]; if (!val) return `- \`${k}\` - unset`; if (isChannelKey(k)) return `- \`${k}\` <#${val}>`; return `- \`${k}\` ${val}`; }); await interaction.reply({ content: `# 📋 Server Configuration\n${lines.join("\n")}`, flags: ["Ephemeral"], }); } export default { data: commandData, execute, autocomplete, };