- Add date_added to all 950+ skills for complete tracking - Update version to 6.5.0 in package.json and README - Regenerate all indexes and catalog - Sync all generated files Features from merged PR #150: - Stars/Upvotes system for community-driven discovery - Auto-update mechanism via START_APP.bat - Interactive Prompt Builder - Date tracking badges - Smart auto-categorization All skills validated and indexed. Made-with: Cursor
283 lines
8.1 KiB
Markdown
283 lines
8.1 KiB
Markdown
---
|
|
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."
|
|
risk: unknown
|
|
source: "vibeship-spawner-skills (Apache 2.0)"
|
|
date_added: "2026-02-27"
|
|
---
|
|
|
|
# Discord Bot Architect
|
|
|
|
## Patterns
|
|
|
|
### Discord.js v14 Foundation
|
|
|
|
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']
|
|
|
|
```javascript
|
|
```javascript
|
|
// 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);
|
|
```
|
|
|
|
```javascript
|
|
// 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`);
|
|
}
|
|
};
|
|
```
|
|
|
|
```javascript
|
|
// src/events/interactionCreate.js
|
|
const { Events } = require('discord.js');
|
|
|
|
module.exports = {
|
|
name: Event
|
|
```
|
|
|
|
### Pycord Bot Foundation
|
|
|
|
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']
|
|
|
|
```python
|
|
```python
|
|
# 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"])
|
|
```
|
|
|
|
```python
|
|
# 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.
|
|
```
|
|
|
|
### Interactive Components Pattern
|
|
|
|
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']
|
|
|
|
```python
|
|
```javascript
|
|
// 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.custo
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
### ❌ Message Content for Commands
|
|
|
|
**Why bad**: Message Content Intent is privileged and deprecated for bot commands.
|
|
Slash commands are the intended approach.
|
|
|
|
### ❌ Syncing Commands on Every Start
|
|
|
|
**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.
|
|
|
|
### ❌ Blocking the Event Loop
|
|
|
|
**Why bad**: Discord gateway requires regular heartbeats. Blocking operations
|
|
cause missed heartbeats and disconnections.
|
|
|
|
## ⚠️ Sharp Edges
|
|
|
|
| Issue | Severity | Solution |
|
|
|-------|----------|----------|
|
|
| Issue | critical | ## Acknowledge immediately, process later |
|
|
| Issue | critical | ## Step 1: Enable in Developer Portal |
|
|
| Issue | high | ## Use a separate deploy script (not on startup) |
|
|
| Issue | critical | ## Never hardcode tokens |
|
|
| Issue | high | ## Generate correct invite URL |
|
|
| Issue | medium | ## Development: Use guild commands |
|
|
| Issue | medium | ## Never block the event loop |
|
|
| Issue | medium | ## Show modal immediately |
|
|
|
|
## When to Use
|
|
This skill is applicable to execute the workflow or actions described in the overview.
|