git clone https://github.com/vibeforge1111/vibeship-spawner-skills
integrations/discord-bot-architect/skill.yamlDiscord Bot Architect Skill
Expert-level Discord bot development with Discord.js v14 and Pycord
id: discord-bot-architect name: Discord Bot Architect description: | Specialized skill for building production-ready Discord bots. Covers Discord.js (JavaScript) and Pycord (Python), gateway intents, slash commands, interactive components, rate limiting, and sharding.
version: 1.0.0 category: integrations tags:
- discord
- bots
- slash-commands
- interactions
- real-time
principles:
- Slash commands over message parsing (Message Content Intent deprecated)
- Acknowledge interactions within 3 seconds, always
- Request only required intents (minimize privileged intents)
- Handle rate limits gracefully with exponential backoff
- Plan for sharding from the start (required at 2500+ guilds)
- Use components (buttons, selects, modals) for rich UX
- Test with guild commands first, deploy global when ready
patterns:
-
name: Discord.js v14 Foundation description: Modern Discord bot setup with Discord.js v14 and slash commands when_to_use:
- Building Discord bots with JavaScript/TypeScript
- Need full gateway connection with events
- Building bots with complex interactions structure: | discord-bot/ ├── src/ │ ├── index.js # Main entry point │ ├── deploy-commands.js # Command registration script │ ├── commands/ # Slash command handlers │ │ └── ping.js │ └── events/ # Event handlers │ ├── ready.js │ └── interactionCreate.js ├── .env └── package.json implementation: |
// src/index.js const { Client, Collection, GatewayIntentBits, Events } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); require('dotenv').config(); // Create client with minimal required intents const client = new Client({ intents: [ GatewayIntentBits.Guilds, // Add only what you need: // GatewayIntentBits.GuildMessages, // GatewayIntentBits.MessageContent, // PRIVILEGED - avoid if possible ] }); // Load commands client.commands = new Collection(); const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js')); for (const file of commandFiles) { const filePath = path.join(commandsPath, file); const command = require(filePath); if ('data' in command && 'execute' in command) { client.commands.set(command.data.name, command); } } // Load events const eventsPath = path.join(__dirname, 'events'); const eventFiles = fs.readdirSync(eventsPath).filter(f => f.endsWith('.js')); for (const file of eventFiles) { const filePath = path.join(eventsPath, file); const event = require(filePath); if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { client.on(event.name, (...args) => event.execute(...args)); } } client.login(process.env.DISCORD_TOKEN);// src/commands/ping.js const { SlashCommandBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('ping') .setDescription('Replies with Pong!'), async execute(interaction) { const sent = await interaction.reply({ content: 'Pinging...', fetchReply: true }); const latency = sent.createdTimestamp - interaction.createdTimestamp; await interaction.editReply(`Pong! Latency: ${latency}ms`); } };// src/events/interactionCreate.js const { Events } = require('discord.js'); module.exports = { name: Events.InteractionCreate, async execute(interaction) { if (!interaction.isChatInputCommand()) return; const command = interaction.client.commands.get(interaction.commandName); if (!command) { console.error(`No command matching ${interaction.commandName}`); return; } try { await command.execute(interaction); } catch (error) { console.error(error); const reply = { content: 'There was an error executing this command!', ephemeral: true }; if (interaction.replied || interaction.deferred) { await interaction.followUp(reply); } else { await interaction.reply(reply); } } } };// src/deploy-commands.js const { REST, Routes } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); require('dotenv').config(); const commands = []; const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js')); for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); commands.push(command.data.toJSON()); } const rest = new REST().setToken(process.env.DISCORD_TOKEN); (async () => { try { console.log(`Refreshing ${commands.length} commands...`); // Guild commands (instant, for testing) // const data = await rest.put( // Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), // { body: commands } // ); // Global commands (can take up to 1 hour to propagate) const data = await rest.put( Routes.applicationCommands(process.env.CLIENT_ID), { body: commands } ); console.log(`Successfully registered ${data.length} commands`); } catch (error) { console.error(error); } })();example: |
.env
DISCORD_TOKEN=your_bot_token CLIENT_ID=your_application_id
package.json dependencies
{ "dependencies": { "discord.js": "^14.14.1", "dotenv": "^16.3.1" } }
-
name: Pycord Bot Foundation description: Discord bot with Pycord (Python) and application commands when_to_use:
- Building Discord bots with Python
- Prefer async/await patterns
- Need good slash command support structure: | discord-bot/ ├── main.py # Main bot file ├── cogs/ # Command groups │ └── general.py ├── .env └── requirements.txt implementation: |
# main.py import os import discord from discord.ext import commands from dotenv import load_dotenv load_dotenv() # Configure intents - only enable what you need intents = discord.Intents.default() # intents.message_content = True # PRIVILEGED - avoid if possible # intents.members = True # PRIVILEGED bot = commands.Bot( command_prefix="!", # Legacy, prefer slash commands intents=intents ) @bot.event async def on_ready(): print(f"Logged in as {bot.user}") # Sync commands (do this carefully - see sharp edges) # await bot.sync_commands() # Slash command @bot.slash_command(name="ping", description="Check bot latency") async def ping(ctx: discord.ApplicationContext): latency = round(bot.latency * 1000) await ctx.respond(f"Pong! Latency: {latency}ms") # Slash command with options @bot.slash_command(name="greet", description="Greet a user") async def greet( ctx: discord.ApplicationContext, user: discord.Option(discord.Member, "User to greet"), message: discord.Option(str, "Custom message", required=False) ): msg = message or "Hello!" await ctx.respond(f"{user.mention}, {msg}") # Load cogs for filename in os.listdir("./cogs"): if filename.endswith(".py"): bot.load_extension(f"cogs.{filename[:-3]}") bot.run(os.environ["DISCORD_TOKEN"])# cogs/general.py import discord from discord.ext import commands class General(commands.Cog): def __init__(self, bot): self.bot = bot @commands.slash_command(name="info", description="Bot information") async def info(self, ctx: discord.ApplicationContext): embed = discord.Embed( title="Bot Info", description="A helpful Discord bot", color=discord.Color.blue() ) embed.add_field(name="Servers", value=len(self.bot.guilds)) embed.add_field(name="Latency", value=f"{round(self.bot.latency * 1000)}ms") await ctx.respond(embed=embed) @commands.Cog.listener() async def on_member_join(self, member: discord.Member): # Requires Members intent (PRIVILEGED) channel = member.guild.system_channel if channel: await channel.send(f"Welcome {member.mention}!") def setup(bot): bot.add_cog(General(bot))example: |
requirements.txt
py-cord>=2.6.0 python-dotenv>=1.0.0
-
name: Interactive Components Pattern description: Using buttons, select menus, and modals for rich UX when_to_use:
- Need interactive user interfaces
- Collecting user input beyond slash command options
- Building menus, confirmations, or forms implementation: |
// Discord.js - Buttons and Select Menus const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('menu') .setDescription('Shows an interactive menu'), async execute(interaction) { // Button row const buttonRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() .setCustomId('confirm') .setLabel('Confirm') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId('cancel') .setLabel('Cancel') .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setLabel('Documentation') .setURL('https://discord.js.org') .setStyle(ButtonStyle.Link) // Link buttons don't emit events ); // Select menu row (one per row, takes all 5 slots) const selectRow = new ActionRowBuilder() .addComponents( new StringSelectMenuBuilder() .setCustomId('select-role') .setPlaceholder('Select a role') .setMinValues(1) .setMaxValues(3) .addOptions([ { label: 'Developer', value: 'dev', emoji: '💻' }, { label: 'Designer', value: 'design', emoji: '🎨' }, { label: 'Community', value: 'community', emoji: '🎉' } ]) ); await interaction.reply({ content: 'Choose an option:', components: [buttonRow, selectRow] }); // Collect responses const collector = interaction.channel.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60_000 // 60 seconds timeout }); collector.on('collect', async i => { if (i.customId === 'confirm') { await i.update({ content: 'Confirmed!', components: [] }); collector.stop(); } else if (i.customId === 'cancel') { await i.update({ content: 'Cancelled', components: [] }); collector.stop(); } else if (i.customId === 'select-role') { await i.update({ content: `You selected: ${i.values.join(', ')}` }); } }); } };// Modals (forms) module.exports = { data: new SlashCommandBuilder() .setName('feedback') .setDescription('Submit feedback'), async execute(interaction) { const modal = new ModalBuilder() .setCustomId('feedback-modal') .setTitle('Submit Feedback'); const titleInput = new TextInputBuilder() .setCustomId('feedback-title') .setLabel('Title') .setStyle(TextInputStyle.Short) .setRequired(true) .setMaxLength(100); const bodyInput = new TextInputBuilder() .setCustomId('feedback-body') .setLabel('Your feedback') .setStyle(TextInputStyle.Paragraph) .setRequired(true) .setMaxLength(1000) .setPlaceholder('Describe your feedback...'); modal.addComponents( new ActionRowBuilder().addComponents(titleInput), new ActionRowBuilder().addComponents(bodyInput) ); // Show modal - MUST be first response await interaction.showModal(modal); } }; // Handle modal submission in interactionCreate if (interaction.isModalSubmit()) { if (interaction.customId === 'feedback-modal') { const title = interaction.fields.getTextInputValue('feedback-title'); const body = interaction.fields.getTextInputValue('feedback-body'); await interaction.reply({ content: `Thanks for your feedback!\n**${title}**\n${body}`, ephemeral: true }); } }# Pycord - Buttons and Views import discord class ConfirmView(discord.ui.View): def __init__(self): super().__init__(timeout=60) self.value = None @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green) async def confirm(self, button, interaction): self.value = True await interaction.response.edit_message(content="Confirmed!", view=None) self.stop() @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) async def cancel(self, button, interaction): self.value = False await interaction.response.edit_message(content="Cancelled", view=None) self.stop() @bot.slash_command(name="confirm") async def confirm_cmd(ctx: discord.ApplicationContext): view = ConfirmView() await ctx.respond("Are you sure?", view=view) await view.wait() # Wait for user interaction if view.value is None: await ctx.followup.send("Timed out") # Select Menu class RoleSelect(discord.ui.Select): def __init__(self): options = [ discord.SelectOption(label="Developer", value="dev", emoji="💻"), discord.SelectOption(label="Designer", value="design", emoji="🎨"), ] super().__init__( placeholder="Select roles...", min_values=1, max_values=2, options=options ) async def callback(self, interaction): await interaction.response.send_message( f"You selected: {', '.join(self.values)}", ephemeral=True ) class RoleView(discord.ui.View): def __init__(self): super().__init__() self.add_item(RoleSelect()) # Modal class FeedbackModal(discord.ui.Modal): def __init__(self): super().__init__(title="Submit Feedback") self.add_item(discord.ui.InputText( label="Title", style=discord.InputTextStyle.short, required=True, max_length=100 )) self.add_item(discord.ui.InputText( label="Feedback", style=discord.InputTextStyle.long, required=True, max_length=1000 )) async def callback(self, interaction): title = self.children[0].value body = self.children[1].value await interaction.response.send_message( f"Thanks!\n**{title}**\n{body}", ephemeral=True ) @bot.slash_command(name="feedback") async def feedback(ctx: discord.ApplicationContext): await ctx.send_modal(FeedbackModal())limits:
- 5 ActionRows per message/modal
- 5 buttons per ActionRow
- 1 select menu per ActionRow (takes all 5 slots)
- 5 select menus max per message
- 25 options per select menu
- Modal must be first response (cannot defer first)
-
name: Deferred Response Pattern description: Handle slow operations without timing out when_to_use:
- Operation takes more than 3 seconds
- Database queries, API calls, LLM responses
- File processing or generation implementation: |
// Discord.js - Deferred response module.exports = { data: new SlashCommandBuilder() .setName('slow-task') .setDescription('Performs a slow operation'), async execute(interaction) { // Defer immediately - you have 3 seconds! await interaction.deferReply(); // For ephemeral: await interaction.deferReply({ ephemeral: true }); try { // Now you have 15 minutes to complete const result = await slowDatabaseQuery(); const aiResponse = await callOpenAI(result); // Edit the deferred reply await interaction.editReply({ content: `Result: ${aiResponse}`, embeds: [resultEmbed] }); } catch (error) { await interaction.editReply({ content: 'An error occurred while processing your request.' }); } } }; // For components (buttons, select menus) collector.on('collect', async i => { await i.deferUpdate(); // Acknowledge without visual change // Or: await i.deferReply({ ephemeral: true }); const result = await slowOperation(); await i.editReply({ content: result }); });# Pycord - Deferred response @bot.slash_command(name="slow-task") async def slow_task(ctx: discord.ApplicationContext): # Defer immediately await ctx.defer() # For ephemeral: await ctx.defer(ephemeral=True) try: result = await slow_database_query() ai_response = await call_openai(result) await ctx.followup.send(f"Result: {ai_response}") except Exception as e: await ctx.followup.send("An error occurred")timing: initial_response: 3 seconds deferred_followup: 15 minutes ephemeral_note: "Can only be set on initial response, not changed later"
-
name: Embed Builder Pattern description: Rich embedded messages for professional-looking content when_to_use:
- Displaying formatted information
- Status updates, help menus, logs
- Data with structure (fields, images) implementation: |
const { EmbedBuilder, Colors } = require('discord.js'); // Basic embed const embed = new EmbedBuilder() .setColor(Colors.Blue) .setTitle('Bot Status') .setURL('https://example.com') .setAuthor({ name: 'Bot Name', iconURL: client.user.displayAvatarURL() }) .setDescription('Current status and statistics') .addFields( { name: 'Servers', value: `${client.guilds.cache.size}`, inline: true }, { name: 'Users', value: `${client.users.cache.size}`, inline: true }, { name: 'Uptime', value: formatUptime(), inline: true } ) .setThumbnail(client.user.displayAvatarURL()) .setImage('https://example.com/banner.png') .setTimestamp() .setFooter({ text: 'Requested by User', iconURL: interaction.user.displayAvatarURL() }); await interaction.reply({ embeds: [embed] }); // Multiple embeds (max 10) await interaction.reply({ embeds: [embed1, embed2, embed3] });# Pycord embed = discord.Embed( title="Bot Status", description="Current status and statistics", color=discord.Color.blue(), url="https://example.com" ) embed.set_author( name="Bot Name", icon_url=bot.user.display_avatar.url ) embed.add_field(name="Servers", value=len(bot.guilds), inline=True) embed.add_field(name="Users", value=len(bot.users), inline=True) embed.set_thumbnail(url=bot.user.display_avatar.url) embed.set_image(url="https://example.com/banner.png") embed.set_footer(text="Requested by User", icon_url=ctx.author.display_avatar.url) embed.timestamp = discord.utils.utcnow() await ctx.respond(embed=embed)limits:
- 10 embeds per message
- 6000 characters total across all embeds
- 256 characters for title
- 4096 characters for description
- 25 fields per embed
- 256 characters per field name
- 1024 characters per field value
-
name: Rate Limit Handling Pattern description: Gracefully handle Discord API rate limits when_to_use:
- High-volume operations
- Bulk messaging or role assignments
- Any repeated API calls implementation: |
// Discord.js handles rate limits automatically, but for custom handling: const { REST } = require('discord.js'); const rest = new REST({ version: '10' }) .setToken(process.env.DISCORD_TOKEN); rest.on('rateLimited', (info) => { console.log(`Rate limited! Retry after ${info.retryAfter}ms`); console.log(`Route: ${info.route}`); console.log(`Global: ${info.global}`); }); // Queue pattern for bulk operations class RateLimitQueue { constructor() { this.queue = []; this.processing = false; this.requestsPerSecond = 40; // Safe margin below 50 } async add(operation) { return new Promise((resolve, reject) => { this.queue.push({ operation, resolve, reject }); this.process(); }); } async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const { operation, resolve, reject } = this.queue.shift(); try { const result = await operation(); resolve(result); } catch (error) { reject(error); } // Throttle: ~40 requests per second await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond)); } this.processing = false; } } const queue = new RateLimitQueue(); // Usage: Send 200 messages without hitting rate limits for (const user of users) { await queue.add(() => user.send('Welcome!')); }# Pycord/discord.py handles rate limits automatically # For custom handling: import asyncio from collections import deque class RateLimitQueue: def __init__(self, requests_per_second=40): self.queue = deque() self.processing = False self.delay = 1 / requests_per_second async def add(self, coro): future = asyncio.Future() self.queue.append((coro, future)) if not self.processing: asyncio.create_task(self._process()) return await future async def _process(self): self.processing = True while self.queue: coro, future = self.queue.popleft() try: result = await coro future.set_result(result) except Exception as e: future.set_exception(e) await asyncio.sleep(self.delay) self.processing = False queue = RateLimitQueue() # Usage for member in guild.members: await queue.add(member.send("Welcome!"))rate_limits: global: 50 requests per second gateway: 120 requests per 60 seconds specific: - "Messages to same channel: 5/5s" - "Bulk delete: 1/1s" - "Guild member requests: varies by guild size"
-
name: Sharding Pattern description: Scale bots to 2500+ servers with sharding when_to_use:
- Bot approaching 2500 guilds (required)
- Want horizontal scaling
- Memory optimization for large bots implementation: |
// Discord.js Sharding Manager // shard.js (main entry) const { ShardingManager } = require('discord.js'); const manager = new ShardingManager('./bot.js', { token: process.env.DISCORD_TOKEN, totalShards: 'auto', // Discord determines optimal count // Or specify: totalShards: 4 }); manager.on('shardCreate', shard => { console.log(`Launched shard ${shard.id}`); shard.on('ready', () => { console.log(`Shard ${shard.id} ready`); }); shard.on('disconnect', () => { console.log(`Shard ${shard.id} disconnected`); }); }); manager.spawn(); // bot.js - Modified for sharding const { Client } = require('discord.js'); const client = new Client({ intents: [...] }); // Get shard info client.on('ready', () => { console.log(`Shard ${client.shard.ids[0]} ready with ${client.guilds.cache.size} guilds`); }); // Cross-shard data async function getTotalGuilds() { const results = await client.shard.fetchClientValues('guilds.cache.size'); return results.reduce((acc, count) => acc + count, 0); } // Broadcast to all shards async function broadcastMessage(channelId, message) { await client.shard.broadcastEval( (c, { channelId, message }) => { const channel = c.channels.cache.get(channelId); if (channel) channel.send(message); }, { context: { channelId, message } } ); }# Pycord - AutoShardedBot import discord from discord.ext import commands # Automatically handles sharding bot = commands.AutoShardedBot( command_prefix="!", intents=discord.Intents.default(), shard_count=None # Auto-determine ) @bot.event async def on_ready(): print(f"Logged in on {len(bot.shards)} shards") for shard_id, shard in bot.shards.items(): print(f"Shard {shard_id}: {shard.latency * 1000:.2f}ms") @bot.event async def on_shard_ready(shard_id): print(f"Shard {shard_id} is ready") # Get guilds per shard for shard_id, guilds in bot.guilds_by_shard().items(): print(f"Shard {shard_id}: {len(guilds)} guilds")scaling_guide:
- "1-2500 guilds: No sharding required"
- "2500+ guilds: Sharding required by Discord"
- "Recommended: ~1000 guilds per shard"
- "Memory: Each shard runs in separate process"
anti_patterns:
-
name: Message Content for Commands description: Don't parse message content for commands why_bad: | Message Content Intent is privileged and deprecated for bot commands. Slash commands are the intended approach. bad_example: | // DON'T - Requires privileged Message Content intent client.on('messageCreate', message => { if (message.content.startsWith('!ping')) { message.reply('Pong!'); } }); good_example: | // DO - Use slash commands await client.application.commands.create({ name: 'ping', description: 'Pong!' });
client.on('interactionCreate', async interaction => { if (interaction.commandName === 'ping') { await interaction.reply('Pong!'); } });
-
name: Syncing Commands on Every Start description: Don't register commands on every bot startup why_bad: | Command registration is rate limited. Global commands take up to 1 hour to propagate. Syncing on every start wastes API calls and can hit limits. bad_example: | // DON'T - Syncs on every restart client.on('ready', async () => { await client.application.commands.set(commands); }); good_example: | // DO - Separate deploy script, run manually // deploy-commands.js await rest.put( Routes.applicationCommands(CLIENT_ID), { body: commands } ); // Run with: node deploy-commands.js
-
name: Blocking the Event Loop description: Don't run synchronous operations in event handlers why_bad: | Discord gateway requires regular heartbeats. Blocking operations cause missed heartbeats and disconnections. bad_example: | client.on('messageCreate', message => { const data = fs.readFileSync('large-file.json'); // BLOCKS // Gateway heartbeat might be missed }); good_example: | client.on('messageCreate', async message => { const data = await fs.promises.readFile('large-file.json'); });
references:
- name: "Discord.js Guide" url: "https://discordjs.guide/"
- name: "Discord.js Documentation" url: "https://discord.js.org/"
- name: "Pycord Guide" url: "https://guide.pycord.dev/"
- name: "Discord Developer Portal" url: "https://discord.com/developers/docs"
- name: "Discord API Documentation" url: "https://discord.com/developers/docs/reference"