From 06f7afe25d1a97ff66e5dfaa13674bbe91c9fb5f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 17:18:28 +0000 Subject: [PATCH] Add /createserver slash command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../arbiter-3.0/src/discord/createserver.js | 295 ++++++++++++++++++ services/arbiter-3.0/src/discord/events.js | 4 + services/arbiter-3.0/src/index.js | 3 +- 3 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 services/arbiter-3.0/src/discord/createserver.js diff --git a/services/arbiter-3.0/src/discord/createserver.js b/services/arbiter-3.0/src/discord/createserver.js new file mode 100644 index 0000000..11aae57 --- /dev/null +++ b/services/arbiter-3.0/src/discord/createserver.js @@ -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 }; diff --git a/services/arbiter-3.0/src/discord/events.js b/services/arbiter-3.0/src/discord/events.js index a323544..33311fb 100644 --- a/services/arbiter-3.0/src/discord/events.js +++ b/services/arbiter-3.0/src/discord/events.js @@ -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', () => { diff --git a/services/arbiter-3.0/src/index.js b/services/arbiter-3.0/src/index.js index f980527..35ef301 100644 --- a/services/arbiter-3.0/src/index.js +++ b/services/arbiter-3.0/src/index.js @@ -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) {