Files
aurorabot/bot/commands/admin/settings.ts
syntaxbullet 141c3098f8 feat: standardize command error handling (Sprint 4)
- Create withCommandErrorHandling utility in bot/lib/commandUtils.ts
- Migrate economy commands: daily, exam, pay, trivia
- Migrate inventory command: use
- Migrate admin/moderation commands: warn, case, cases, clearwarning,
  warnings, note, notes, create_color, listing, webhook, refresh,
  terminal, featureflags, settings, prune
- Add 9 unit tests for the utility
- Update AGENTS.md with new recommended error handling pattern
2026-02-13 14:23:37 +01:00

244 lines
11 KiB
TypeScript

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 { withCommandErrorHandling } from "@lib/commandUtils";
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 withCommandErrorHandling(
interaction,
async () => {
const subcommand = interaction.options.getSubcommand();
const guildId = interaction.guildId!;
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;
}
},
{ ephemeral: true }
);
},
});
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;
}
}
}