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 { handleLinkCommand } = require('./commands');
|
||||||
|
const { handleCreateServerCommand } = require('./createserver');
|
||||||
const discordRoleSync = require('../services/discordRoleSync');
|
const discordRoleSync = require('../services/discordRoleSync');
|
||||||
|
|
||||||
function registerEvents(client) {
|
function registerEvents(client) {
|
||||||
@@ -7,6 +8,9 @@ function registerEvents(client) {
|
|||||||
if (interaction.commandName === 'link') {
|
if (interaction.commandName === 'link') {
|
||||||
await handleLinkCommand(interaction);
|
await handleLinkCommand(interaction);
|
||||||
}
|
}
|
||||||
|
if (interaction.commandName === 'createserver') {
|
||||||
|
await handleCreateServerCommand(interaction);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const webhookRoutes = require('./routes/webhook');
|
|||||||
const stripeRoutes = require('./routes/stripe');
|
const stripeRoutes = require('./routes/stripe');
|
||||||
const { registerEvents } = require('./discord/events');
|
const { registerEvents } = require('./discord/events');
|
||||||
const { linkCommand } = require('./discord/commands');
|
const { linkCommand } = require('./discord/commands');
|
||||||
|
const { createServerCommand } = require('./discord/createserver');
|
||||||
const { initCron } = require('./sync/cron');
|
const { initCron } = require('./sync/cron');
|
||||||
const discordRoleSync = require('./services/discordRoleSync');
|
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.');
|
console.log('Refreshing application (/) commands.');
|
||||||
await rest.put(
|
await rest.put(
|
||||||
Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.GUILD_ID),
|
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.');
|
console.log('✅ Successfully reloaded application (/) commands.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user