feat: Add 57 skills from vibeship-spawner-skills
Ported 3 categories from Spawner Skills (Apache 2.0): - AI Agents (21 skills): langfuse, langgraph, crewai, rag-engineer, etc. - Integrations (25 skills): stripe, firebase, vercel, supabase, etc. - Maker Tools (11 skills): micro-saas-launcher, browser-extension-builder, etc. All skills converted from 4-file YAML to SKILL.md format. Source: https://github.com/vibeforge1111/vibeship-spawner-skills
This commit is contained in:
277
skills/discord-bot-architect/SKILL.md
Normal file
277
skills/discord-bot-architect/SKILL.md
Normal file
@@ -0,0 +1,277 @@
|
||||
---
|
||||
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."
|
||||
source: vibeship-spawner-skills (Apache 2.0)
|
||||
---
|
||||
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user