Add /delserver slash command

Deletes complete server setup:
- All channels in category
- The category itself
- The server role

Requires confirm:True to execute.
Without confirm, shows preview of what would be deleted.
Reminds to clean up Carl-bot reaction roles.

Staff only.

Chronicler: #71
This commit is contained in:
Claude
2026-04-08 17:27:10 +00:00
parent 7ecce5da8f
commit 69200d8ac3
3 changed files with 196 additions and 1 deletions

View File

@@ -0,0 +1,190 @@
/**
* /delserver Command
* Deletes a complete server setup:
* - All channels in the category
* - The category itself
* - The server role
*
* Requires confirm:True to execute.
* Without confirm, shows preview of what would be deleted.
*
* Created: April 8, 2026
* Chronicler: #71
* Task: #98 Discord Channel Automation
*/
const { SlashCommandBuilder, ChannelType } = require('discord.js');
// Staff role names that can use this command
const STAFF_ROLES = ['Staff', '🛡️ Moderator', '👑 The Wizard', '💎 The Emissary', '✨ The Catalyst'];
// Build the slash command
const delServerCommand = new SlashCommandBuilder()
.setName('delserver')
.setDescription('Delete a server setup completely (Staff only)')
.addStringOption(option =>
option.setName('name')
.setDescription('Server name (e.g., "Beyond Depth")')
.setRequired(true)
.setMaxLength(50)
)
.addBooleanOption(option =>
option.setName('confirm')
.setDescription('Set to True to confirm deletion')
.setRequired(false)
);
/**
* Check if user has staff role
*/
function isStaff(member) {
return member.roles.cache.some(role => STAFF_ROLES.includes(role.name));
}
/**
* Handle /delserver command
*/
async function handleDelServerCommand(interaction) {
// Check permissions
if (!isStaff(interaction.member)) {
return interaction.reply({
content: '❌ This command is restricted to Staff members.',
ephemeral: true
});
}
const serverName = interaction.options.getString('name');
const confirmed = interaction.options.getBoolean('confirm') || false;
const guild = interaction.guild;
await interaction.deferReply({ ephemeral: false });
try {
await guild.channels.fetch();
await guild.roles.fetch();
// Find the category (with or without emoji)
const category = guild.channels.cache.find(
ch => ch.type === ChannelType.GuildCategory &&
(ch.name === serverName || ch.name === `🎮 ${serverName}`)
);
// Find the role
const serverRole = guild.roles.cache.find(
r => r.name.toLowerCase() === serverName.toLowerCase()
);
// Build preview
const channelsToDelete = category
? guild.channels.cache.filter(ch => ch.parentId === category.id)
: new Map();
const preview = [];
if (category) {
preview.push(`**Category:** ${category.name}`);
if (channelsToDelete.size > 0) {
preview.push(`**Channels (${channelsToDelete.size}):**`);
channelsToDelete.forEach(ch => {
const typeLabel = ch.type === ChannelType.GuildVoice ? '🔊' :
ch.type === ChannelType.GuildForum ? '💬' : '#';
preview.push(`${typeLabel} ${ch.name}`);
});
}
} else {
preview.push(`**Category:** ⚠️ Not found`);
}
if (serverRole) {
preview.push(`**Role:** @${serverRole.name} (${serverRole.members.size} members)`);
} else {
preview.push(`**Role:** ⚠️ Not found`);
}
// If nothing found
if (!category && !serverRole) {
return interaction.editReply(`❌ Server **${serverName}** not found.\n\nNo category or role matches that name.`);
}
// If not confirmed, show preview
if (!confirmed) {
const previewMessage = `⚠️ **Delete Server: ${serverName}**
This will permanently delete:
${preview.join('\n')}
---
**To confirm, run:**
\`\`\`
/delserver name:${serverName} confirm:True
\`\`\``;
return interaction.editReply(previewMessage);
}
// CONFIRMED - Execute deletion
await interaction.editReply(`🗑️ Deleting **${serverName}**...`);
const deleted = {
channels: 0,
category: false,
role: false
};
// Delete channels first
if (category) {
for (const [id, channel] of channelsToDelete) {
try {
await channel.delete(`/delserver by ${interaction.user.tag}`);
deleted.channels++;
} catch (err) {
console.error(`Failed to delete channel ${channel.name}:`, err.message);
}
}
// Delete category
try {
await category.delete(`/delserver by ${interaction.user.tag}`);
deleted.category = true;
} catch (err) {
console.error(`Failed to delete category:`, err.message);
}
}
// Delete role
if (serverRole) {
try {
await serverRole.delete(`/delserver by ${interaction.user.tag}`);
deleted.role = true;
} catch (err) {
console.error(`Failed to delete role:`, err.message);
}
}
// Success message
const successMessage = `🗑️ **${serverName}** deleted!
**Removed:**
${deleted.channels} channels
${deleted.category ? '1 category' : '0 categories'}
${deleted.role ? '1 role' : '0 roles'}
---
**Don't forget to:**
1. Remove the reaction emoji from <#1403980899464384572>
2. Remove the role mapping from Carl-bot`;
await interaction.editReply(successMessage);
console.log(`🗑️ /delserver: ${serverName} deleted by ${interaction.user.tag}`);
} catch (error) {
console.error('/delserver error:', error);
await interaction.editReply(`❌ Error deleting server: ${error.message}`);
}
}
module.exports = { delServerCommand, handleDelServerCommand };

View File

@@ -1,5 +1,6 @@
const { handleLinkCommand } = require('./commands');
const { handleCreateServerCommand } = require('./createserver');
const { handleDelServerCommand } = require('./delserver');
const discordRoleSync = require('../services/discordRoleSync');
function registerEvents(client) {
@@ -11,6 +12,9 @@ function registerEvents(client) {
if (interaction.commandName === 'createserver') {
await handleCreateServerCommand(interaction);
}
if (interaction.commandName === 'delserver') {
await handleDelServerCommand(interaction);
}
});
client.on('ready', () => {

View File

@@ -17,6 +17,7 @@ const stripeRoutes = require('./routes/stripe');
const { registerEvents } = require('./discord/events');
const { linkCommand } = require('./discord/commands');
const { createServerCommand } = require('./discord/createserver');
const { delServerCommand } = require('./discord/delserver');
const { initCron } = require('./sync/cron');
const discordRoleSync = require('./services/discordRoleSync');
@@ -129,7 +130,7 @@ const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN)
console.log('Refreshing application (/) commands.');
await rest.put(
Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.GUILD_ID),
{ body: [linkCommand.toJSON(), createServerCommand.toJSON()] },
{ body: [linkCommand.toJSON(), createServerCommand.toJSON(), delServerCommand.toJSON()] },
);
console.log('✅ Successfully reloaded application (/) commands.');
} catch (error) {