Add /createserver slash command

Creates complete server setup with one command:
- Creates role
- Creates category with 🎮 prefix
- Creates chat, in-game, forum, voice channels
- Applies permission template
- Posts and archives welcome message
- Suggests unused emoji for reaction roles

Staff only. Reminds to configure Carl-bot.

Task #98 Discord Channel Automation
Chronicler: #71
This commit is contained in:
Claude
2026-04-08 17:18:28 +00:00
parent 083885c874
commit 06f7afe25d
3 changed files with 301 additions and 1 deletions

View File

@@ -0,0 +1,295 @@
/**
* /createserver Command
* Creates a complete server setup with one command:
* - Role
* - Category with emoji prefix
* - Chat, in-game, forum, voice channels
* - Permission template
* - Welcome post (archived)
* - Suggests emoji for reaction roles
*
* Created: April 8, 2026
* Chronicler: #71
* Task: #98 Discord Channel Automation
*/
const { SlashCommandBuilder, ChannelType, PermissionFlagsBits, PermissionsBitField } = require('discord.js');
// Channel ID for #get-roles
const GET_ROLES_CHANNEL_ID = '1403980899464384572';
// Staff role names that can use this command
const STAFF_ROLES = ['Staff', '🛡️ Moderator', '👑 The Wizard', '💎 The Emissary', '✨ The Catalyst'];
// Admin roles that get full access to new server channels
const ADMIN_ROLES = ['Staff', '🛡️ Moderator', '👑 The Wizard', '💎 The Emissary', '✨ The Catalyst'];
// Standard forum tags
const STANDARD_FORUM_TAGS = [
{ name: 'Builds', emoji: '🏗️' },
{ name: 'Help', emoji: '❓' },
{ name: 'Suggestion', emoji: '💡' },
{ name: 'Bug Report', emoji: '🐛' },
{ name: 'Achievement', emoji: '🎉' },
{ name: 'Guide', emoji: '📖' }
];
// Emoji pool for reaction role suggestions (gaming/server themed)
const EMOJI_POOL = [
'🎮', '🕹️', '⚔️', '🛡️', '🏰', '🗡️', '🔮', '🧙', '🐉', '🌋',
'🌊', '🏔️', '🌲', '🍄', '⚡', '💎', '🪨', '⛏️', '🧱', '🏠',
'🌙', '☀️', '🌟', '✨', '🎯', '🎪', '🎭', '🎨', '🧪', '🔧',
'⚙️', '🚀', '👾', '🤖', '🎲', '🃏', '🏴‍☠️', '⚓', '🧭', '🗺️',
'🦊', '🐺', '🦁', '🐲', '🦅', '🐋', '🦈', '🐙', '🦑', '🕷️',
'🌸', '🌺', '🌻', '🍀', '🌿', '🔥', '❄️', '💧', '🌪️', '⭐'
];
// Generic welcome post template
const WELCOME_TEMPLATE = (serverName) => `🎮 **Welcome to ${serverName}!**
This is your community space for all things ${serverName}. Share your adventures, ask questions, and connect with fellow players!
**This forum is your space to:**
- 🏗️ Share your builds and creations
- ❓ Ask for help and advice
- 💡 Suggest improvements
- 🎉 Celebrate your achievements
---
**🎮 First Challenge: Introduce Yourself!**
Tell us about your playstyle! What are you most excited to try on this server?
*Welcome to Firefrost Gaming!* 🔥❄️`;
// Build the slash command
const createServerCommand = new SlashCommandBuilder()
.setName('createserver')
.setDescription('Create a complete server setup (Staff only)')
.addStringOption(option =>
option.setName('name')
.setDescription('Server name (e.g., "Beyond Depth")')
.setRequired(true)
.setMaxLength(50)
);
/**
* Slugify a server name for channel names
*/
function slugify(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.substring(0, 100);
}
/**
* Check if user has staff role
*/
function isStaff(member) {
return member.roles.cache.some(role => STAFF_ROLES.includes(role.name));
}
/**
* Get unused emoji from pool
*/
async function getUnusedEmoji(channel) {
try {
const message = await channel.messages.fetch({ limit: 50 });
const getRolesMsg = message.find(m => m.reactions.cache.size > 0);
if (!getRolesMsg) {
// No message with reactions found, return first emoji
return EMOJI_POOL[0];
}
const usedEmojis = new Set();
getRolesMsg.reactions.cache.forEach(reaction => {
usedEmojis.add(reaction.emoji.name);
});
// Find first unused emoji
for (const emoji of EMOJI_POOL) {
if (!usedEmojis.has(emoji)) {
return emoji;
}
}
// All emojis used, return a random one with note
return EMOJI_POOL[Math.floor(Math.random() * EMOJI_POOL.length)];
} catch (error) {
console.error('Error fetching emojis:', error);
return EMOJI_POOL[0];
}
}
/**
* Handle /createserver command
*/
async function handleCreateServerCommand(interaction) {
// Check permissions
if (!isStaff(interaction.member)) {
return interaction.reply({
content: '❌ This command is restricted to Staff members.',
ephemeral: true
});
}
await interaction.deferReply({ ephemeral: false });
const serverName = interaction.options.getString('name');
const guild = interaction.guild;
try {
// Fetch roles
await guild.roles.fetch();
// Check if role already exists
const existingRole = guild.roles.cache.find(r => r.name.toLowerCase() === serverName.toLowerCase());
if (existingRole) {
return interaction.editReply(`❌ Role **${serverName}** already exists!`);
}
// Check if category already exists
const existingCategory = guild.channels.cache.find(
ch => ch.type === ChannelType.GuildCategory &&
(ch.name === serverName || ch.name === `🎮 ${serverName}`)
);
if (existingCategory) {
return interaction.editReply(`❌ Category **${serverName}** already exists!`);
}
// Get key roles
const everyoneRole = guild.roles.everyone;
const wandererRole = guild.roles.cache.find(r => r.name === 'Wanderer');
if (!wandererRole) {
return interaction.editReply('❌ Wanderer role not found!');
}
// Get admin role IDs
const adminRoleIds = ADMIN_ROLES
.map(name => guild.roles.cache.find(r => r.name === name)?.id)
.filter(Boolean);
// Progress update
await interaction.editReply(`⏳ Creating **${serverName}**...`);
// Step 1: Create role
const serverRole = await guild.roles.create({
name: serverName,
reason: `/createserver by ${interaction.user.tag}`
});
// Step 2: Build permission overwrites
const permissionOverwrites = [
{
id: everyoneRole.id,
deny: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.Connect]
},
{
id: wandererRole.id,
allow: [PermissionFlagsBits.ViewChannel],
deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.Connect]
},
{
id: serverRole.id,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.Connect, PermissionFlagsBits.ReadMessageHistory]
},
...adminRoleIds.map(roleId => ({
id: roleId,
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.Connect, PermissionFlagsBits.ReadMessageHistory]
}))
];
// Step 3: Create category
const category = await guild.channels.create({
name: `🎮 ${serverName}`,
type: ChannelType.GuildCategory,
permissionOverwrites,
reason: `/createserver by ${interaction.user.tag}`
});
// Step 4: Create chat channel
const slug = slugify(serverName);
await guild.channels.create({
name: `${slug}-chat`,
type: ChannelType.GuildText,
parent: category.id,
topic: `General chat for ${serverName}`,
reason: `/createserver by ${interaction.user.tag}`
});
// Step 5: Create in-game channel
await guild.channels.create({
name: `${slug}-in-game`,
type: ChannelType.GuildText,
parent: category.id,
topic: `In-game chat bridge for ${serverName}`,
reason: `/createserver by ${interaction.user.tag}`
});
// Step 6: Create forum
const forum = await guild.channels.create({
name: `${slug}-forum`,
type: ChannelType.GuildForum,
parent: category.id,
topic: `Discussion forum for ${serverName}`,
availableTags: STANDARD_FORUM_TAGS.map(tag => ({
name: tag.name,
emoji: { name: tag.emoji }
})),
reason: `/createserver by ${interaction.user.tag}`
});
// Step 7: Create voice channel
await guild.channels.create({
name: serverName,
type: ChannelType.GuildVoice,
parent: category.id,
reason: `/createserver by ${interaction.user.tag}`
});
// Step 8: Post welcome message
const welcomeThread = await forum.threads.create({
name: `Welcome to ${serverName}!`,
message: { content: WELCOME_TEMPLATE(serverName) },
reason: `/createserver by ${interaction.user.tag}`
});
// Step 9: Archive the welcome post
await welcomeThread.setArchived(true, 'Auto-archive welcome post');
// Step 10: Get suggested emoji
const getRolesChannel = await guild.channels.fetch(GET_ROLES_CHANNEL_ID);
const suggestedEmoji = await getUnusedEmoji(getRolesChannel);
// Success message
const successMessage = `✅ **${serverName}** created!
**Created:**
• Role: ${serverRole}
• Category: 🎮 ${serverName}
• Channels: ${slug}-chat, ${slug}-in-game, ${slug}-forum, voice
• Welcome post: Archived ✓
---
**Suggested emoji for #get-roles:** ${suggestedEmoji}
To complete setup, add ${suggestedEmoji} as a reaction to the #get-roles message, then configure Carl-bot to assign the "${serverName}" role.`;
await interaction.editReply(successMessage);
console.log(`✅ /createserver: ${serverName} created by ${interaction.user.tag}`);
} catch (error) {
console.error('/createserver error:', error);
await interaction.editReply(`❌ Error creating server: ${error.message}`);
}
}
module.exports = { createServerCommand, handleCreateServerCommand };

View File

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

View File

@@ -16,6 +16,7 @@ const webhookRoutes = require('./routes/webhook');
const stripeRoutes = require('./routes/stripe');
const { registerEvents } = require('./discord/events');
const { linkCommand } = require('./discord/commands');
const { createServerCommand } = require('./discord/createserver');
const { initCron } = require('./sync/cron');
const discordRoleSync = require('./services/discordRoleSync');
@@ -128,7 +129,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()] },
{ body: [linkCommand.toJSON(), createServerCommand.toJSON()] },
);
console.log('✅ Successfully reloaded application (/) commands.');
} catch (error) {