diff --git a/bot/commands/admin/settings.ts b/bot/commands/admin/settings.ts new file mode 100644 index 0000000..79da5b6 --- /dev/null +++ b/bot/commands/admin/settings.ts @@ -0,0 +1,247 @@ +import { createCommand } from "@shared/lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, Colors, ChatInputCommandInteraction } from "discord.js"; +import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service"; +import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds"; +import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config"; +import { UserError } from "@shared/lib/errors"; + +export const settings = createCommand({ + data: new SlashCommandBuilder() + .setName("settings") + .setDescription("Manage guild settings") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(sub => + sub.setName("show") + .setDescription("Show current guild settings")) + .addSubcommand(sub => + sub.setName("set") + .setDescription("Set a guild setting") + .addStringOption(opt => + opt.setName("key") + .setDescription("Setting to change") + .setRequired(true) + .addChoices( + { name: "Student Role", value: "studentRole" }, + { name: "Visitor Role", value: "visitorRole" }, + { name: "Welcome Channel", value: "welcomeChannel" }, + { name: "Welcome Message", value: "welcomeMessage" }, + { name: "Feedback Channel", value: "feedbackChannel" }, + { name: "Terminal Channel", value: "terminalChannel" }, + { name: "Terminal Message", value: "terminalMessage" }, + { name: "Moderation Log Channel", value: "moderationLogChannel" }, + { name: "DM on Warn", value: "moderationDmOnWarn" }, + { name: "Auto Timeout Threshold", value: "moderationAutoTimeoutThreshold" }, + )) + .addRoleOption(opt => + opt.setName("role") + .setDescription("Role value")) + .addChannelOption(opt => + opt.setName("channel") + .setDescription("Channel value")) + .addStringOption(opt => + opt.setName("text") + .setDescription("Text value")) + .addIntegerOption(opt => + opt.setName("number") + .setDescription("Number value")) + .addBooleanOption(opt => + opt.setName("boolean") + .setDescription("Boolean value (true/false)"))) + .addSubcommand(sub => + sub.setName("reset") + .setDescription("Reset a setting to default") + .addStringOption(opt => + opt.setName("key") + .setDescription("Setting to reset") + .setRequired(true) + .addChoices( + { name: "Student Role", value: "studentRole" }, + { name: "Visitor Role", value: "visitorRole" }, + { name: "Welcome Channel", value: "welcomeChannel" }, + { name: "Welcome Message", value: "welcomeMessage" }, + { name: "Feedback Channel", value: "feedbackChannel" }, + { name: "Terminal Channel", value: "terminalChannel" }, + { name: "Terminal Message", value: "terminalMessage" }, + { name: "Moderation Log Channel", value: "moderationLogChannel" }, + { name: "DM on Warn", value: "moderationDmOnWarn" }, + { name: "Auto Timeout Threshold", value: "moderationAutoTimeoutThreshold" }, + ))) + .addSubcommand(sub => + sub.setName("colors") + .setDescription("Manage color roles") + .addStringOption(opt => + opt.setName("action") + .setDescription("Action to perform") + .setRequired(true) + .addChoices( + { name: "List", value: "list" }, + { name: "Add", value: "add" }, + { name: "Remove", value: "remove" }, + )) + .addRoleOption(opt => + opt.setName("role") + .setDescription("Role to add/remove") + .setRequired(false))), + + execute: async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId!; + + try { + switch (subcommand) { + case "show": + await handleShow(interaction, guildId); + break; + case "set": + await handleSet(interaction, guildId); + break; + case "reset": + await handleReset(interaction, guildId); + break; + case "colors": + await handleColors(interaction, guildId); + break; + } + } catch (error) { + if (error instanceof UserError) { + await interaction.editReply({ embeds: [createErrorEmbed(error.message)] }); + } else { + throw error; + } + } + }, +}); + +async function handleShow(interaction: ChatInputCommandInteraction, guildId: string) { + const settings = await getGuildConfig(guildId); + + const colorRolesDisplay = settings.colorRoles?.length + ? settings.colorRoles.map(id => `<@&${id}>`).join(", ") + : "None"; + + const embed = createBaseEmbed("Guild Settings", undefined, Colors.Blue) + .addFields( + { name: "Student Role", value: settings.studentRole ? `<@&${settings.studentRole}>` : "Not set", inline: true }, + { name: "Visitor Role", value: settings.visitorRole ? `<@&${settings.visitorRole}>` : "Not set", inline: true }, + { name: "\u200b", value: "\u200b", inline: true }, + { name: "Welcome Channel", value: settings.welcomeChannelId ? `<#${settings.welcomeChannelId}>` : "Not set", inline: true }, + { name: "Feedback Channel", value: settings.feedbackChannelId ? `<#${settings.feedbackChannelId}>` : "Not set", inline: true }, + { name: "Moderation Log", value: settings.moderation?.cases?.logChannelId ? `<#${settings.moderation.cases.logChannelId}>` : "Not set", inline: true }, + { name: "Terminal Channel", value: settings.terminal?.channelId ? `<#${settings.terminal.channelId}>` : "Not set", inline: true }, + { name: "DM on Warn", value: settings.moderation?.cases?.dmOnWarn !== false ? "Enabled" : "Disabled", inline: true }, + { name: "Auto Timeout", value: settings.moderation?.cases?.autoTimeoutThreshold ? `${settings.moderation.cases.autoTimeoutThreshold} warnings` : "Disabled", inline: true }, + { name: "Color Roles", value: colorRolesDisplay, inline: false }, + ); + + if (settings.welcomeMessage) { + embed.addFields({ name: "Welcome Message", value: settings.welcomeMessage.substring(0, 1024), inline: false }); + } + + await interaction.editReply({ embeds: [embed] }); +} + +async function handleSet(interaction: ChatInputCommandInteraction, guildId: string) { + const key = interaction.options.getString("key", true); + const role = interaction.options.getRole("role"); + const channel = interaction.options.getChannel("channel"); + const text = interaction.options.getString("text"); + const number = interaction.options.getInteger("number"); + const boolean = interaction.options.getBoolean("boolean"); + + let value: string | number | boolean | null = null; + + if (role) value = role.id; + else if (channel) value = channel.id; + else if (text) value = text; + else if (number !== null) value = number; + else if (boolean !== null) value = boolean; + + if (value === null) { + await interaction.editReply({ + embeds: [createErrorEmbed("Please provide a role, channel, text, number, or boolean value")] + }); + return; + } + + await guildSettingsService.updateSetting(guildId, key, value); + invalidateGuildConfigCache(guildId); + + await interaction.editReply({ + embeds: [createSuccessEmbed(`Setting "${key}" updated`)] + }); +} + +async function handleReset(interaction: ChatInputCommandInteraction, guildId: string) { + const key = interaction.options.getString("key", true); + + await guildSettingsService.updateSetting(guildId, key, null); + invalidateGuildConfigCache(guildId); + + await interaction.editReply({ + embeds: [createSuccessEmbed(`Setting "${key}" reset to default`)] + }); +} + +async function handleColors(interaction: ChatInputCommandInteraction, guildId: string) { + const action = interaction.options.getString("action", true); + const role = interaction.options.getRole("role"); + + switch (action) { + case "list": { + const settings = await getGuildConfig(guildId); + const colorRoles = settings.colorRoles ?? []; + + if (colorRoles.length === 0) { + await interaction.editReply({ + embeds: [createBaseEmbed("Color Roles", "No color roles configured.", Colors.Blue)] + }); + return; + } + + const embed = createBaseEmbed("Color Roles", undefined, Colors.Blue) + .addFields({ + name: `Configured Roles (${colorRoles.length})`, + value: colorRoles.map(id => `<@&${id}>`).join("\n"), + }); + + await interaction.editReply({ embeds: [embed] }); + break; + } + + case "add": { + if (!role) { + await interaction.editReply({ + embeds: [createErrorEmbed("Please specify a role to add.")] + }); + return; + } + + await guildSettingsService.addColorRole(guildId, role.id); + invalidateGuildConfigCache(guildId); + + await interaction.editReply({ + embeds: [createSuccessEmbed(`Added <@&${role.id}> to color roles.`)] + }); + break; + } + + case "remove": { + if (!role) { + await interaction.editReply({ + embeds: [createErrorEmbed("Please specify a role to remove.")] + }); + return; + } + + await guildSettingsService.removeColorRole(guildId, role.id); + invalidateGuildConfigCache(guildId); + + await interaction.editReply({ + embeds: [createSuccessEmbed(`Removed <@&${role.id}> from color roles.`)] + }); + break; + } + } +}