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:
295
services/arbiter-3.0/src/discord/createserver.js
Normal file
295
services/arbiter-3.0/src/discord/createserver.js
Normal 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 };
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user