npx agentspec-cli install bfab4e70-c204-4ca9-b915-b39071070aea --framework ClaudeCode→ .claude/skills/discord/SKILL.md
# Discord Bot Skill
## Overview
**Discord** is a real-time communication platform featuring voice, video, and text channels. Bots extend Discord with automated capabilities — moderation, notifications, alerts, and custom workflows.
This skill covers the **discord.py** library, which is Python's most popular Discord bot framework.
- **Repository**: [discord.py](https://github.com/Rapptz/discord.py)
- **Documentation**: [discordpy.readthedocs.io](https://discordpy.readthedocs.io/)
- **Discord Developer Portal**: [discord.com/developers/applications](https://discord.com/developers/applications)
- **Python**: 3.8+
- **Latest Version**: 2.0+ (rewrite with slash commands as primary)
## Installation
```bash
pip install discord.py
```
For voice support:
```bash
pip install "discord.py[voice]"
```
## Discord Developer Portal Setup
Before writing any code, you must create a bot at the Discord Developer Portal:
1. Go to [discord.com/developers/applications](https://discord.com/developers/applications)
2. Click **New Application** → name it
3. Under **Bot** → click **Reset Token** to get your bot token
4. Enable **Message Content Intent** (under Bot → Privileged Gateway Intents) for reading message content
5. Enable **Server Members Intent** for member management
6. Under **OAuth2 → URL Generator**:
- Select scopes: `bot`, `applications.commands`
- Select permissions: `Send Messages`, `Manage Messages`, `Embed Links`, etc.
7. Use the generated URL to invite the bot to your server
**⚠️ Never expose your bot token.** Store it in environment variables.
```python
import os
import discord
BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
```
## Core Concepts
### Intents
Intents declare what events your bot wants to receive. Without them, events are filtered out.
```python
intents = discord.Intents.default()
intents.message_content = True # Required to read message text
intents.members = True # Required for member events
intents.guilds = True # Required for guild/server events
client = discord.Client(intents=intents)
```
### Guilds vs Channels
Discord's hierarchy:
```
Guild (Server)
├── Categories (folders)
│ ├── Text Channels
│ ├── Voice Channels
│ └── Stage Channels
└── Roles & Members
```
## Client Setup
### Basic Bot Client
```python
import discord
from discord import Intents
intents = Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Bot logged in as {client.user}")
client.run("YOUR_BOT_TOKEN")
```
### Bot with Commands (ext.commands)
The commands extension provides prefix commands alongside slash commands.
```python
import discord
from discord.ext import commands
intents = Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.command()
async def ping(ctx):
"""Respond with bot latency."""
await ctx.send(f"Pong! {round(bot.latency * 1000)}ms")
@bot.command()
async def hello(ctx):
"""Greet the user."""
await ctx.send(f"Hello {ctx.author.mention}! 👋")
bot.run("YOUR_BOT_TOKEN")
```
### Using a Config File (Recommended)
Create a `config.py` or `.env` file — never commit tokens.
```python
# config.py
import os
from dataclasses import dataclass
@dataclass
class DiscordConfig:
token: str
guild_id: int
log_channel_id: int
@classmethod
def from_env(cls):
return cls(
token=os.environ["DISCORD_BOT_TOKEN"],
guild_id=int(os.environ.get("DISCORD_GUILD_ID", 0)),
log_channel_id=int(os.environ.get("DISCORD_LOG_CHANNEL_ID", 0)),
)
config = DiscordConfig.from_env()
```
## Slash Commands
Slash commands are Discord's native command interface. They appear when users type `/`.
### Global vs Guild Commands
- **Global**: Available in all servers the bot is in
- **Guild**: Available only in specific servers (instant registration, faster testing)
```python
import discord
from discord import app_commands
intents = Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
# Global slash command
@tree.command(name="ping", description="Check bot latency")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message(f"Pong! {round(client.latency * 1000)}ms")
# Guild slash command (instant, for testing)
GUILD_ID = discord.Object(id=123456789)
@tree.command(name="test", description="Test command", guild=GUILD_ID)
async def test(interaction: discord.Interaction):
await interaction.response.send_message("Testing!")
# Sync commands (called once to register them)
@client.event
async def on_ready():
await tree.sync() # Global commands
# await tree.sync(guild=GUILD_ID) # Guild-specific
print(f"Logged in as {client.user}")
```
### Slash Command Options
```python
@tree.command(name="report", description="Report an issue")
@app_commands.describe(
title="Brief title for the issue",
description="Detailed description",
severity="How critical is this?",
)
@app_commands.choices(severity=[
app_commands.Choice(name="Low", value=1),
app_commands.Choice(name="Medium", value=2),
app_commands.Choice(name="High", value=3),
app_commands.Choice(name="Critical", value=4),
])
async def report(
interaction: discord.Interaction,
title: str,
description: str,
severity: app_commands.Choice[int],
):
embed = discord.Embed(title=title, description=description)
embed.add_field(name="Severity", value=severity.name)
await interaction.response.send_message(embed=embed)
```
### Subcommands and Groups
```python
@tree.group(name="moderation", description="Moderation commands")
async def moderation(interaction: discord.Interaction):
pass # Group itself does nothing
@moderation.command(name="kick", description="Kick a user")
@app_commands.describe(user="User to kick", reason="Reason")
async def kick(interaction: discord.Interaction, user: discord.Member, reason: str = "No reason"):
await user.kick(reason=reason)
await interaction.response.send_message(f"Kicked {user.mention}")
```
## @Mentions and Roles
### Sending Mentionable Messages
```python
# Mention a specific user
await channel.send(f"Hello {user.mention}!")
# Mention a role
await channel.send(f"Attention {role.mention}!")
# Mention everyone in a role
for member in role.members:
await channel.send(f"{member.mention}")
```
### Role-Based Access
```python
MOD_ROLE_ID = 123456789
@tree.command(name="cleanup", description="Delete messages in channel")
@app_commands.check(lambda interaction: any(
role.id == MOD_ROLE_ID for role in interaction.user.roles
))
async def cleanup(interaction: discord.Interaction, count: int = 10):
if count > 100:
count = 100
deleted = await interaction.channel.purge(limit=count)
await interaction.response.send_message(f"Deleted {len(deleted)} messages")
```
### Dynamic Role Assignment
```python
@tree.command(name="assign", description="Assign yourself a role")
@app_commands.describe(role="Role to assign")
async def assign(interaction: discord.Interaction, role: discord.Role):
# Prevent assigning admin roles
if role.permissions.administrator:
await interaction.response.send_message("Cannot assign admin roles!", ephemeral=True)
return
await interaction.user.add_roles(role)
await interaction.response.send_message(f"Assigned {role.mention} to you!", ephemeral=True)
@tree.command(name="unassign", description="Remove a role from yourself")
@app_commands.describe(role="Role to remove")
async def unassign(interaction: discord.Interaction, role: discord.Role):
await interaction.user.remove_roles(role)
await interaction.response.send_message(f"Removed {role.mention} from you!", ephemeral=True)
```
## Text Channels and Threads
### Reading and Sending Messages
```python
@tree.command(name="status", description="Check project status")
async def status(interaction: discord.Interaction):
channel = interaction.guild.get_channel(123456789) # status channel
messages = []
async for message in channel.history(limit=10):
messages.append(f"[{message.created_at}] {message.author}: {message.content}")
await interaction.response.send_message(
"\n".join(messages) or "No messages",
ephemeral=True # Only visible to the user
)
```
### Thread Operations
```python
# Create a thread
@tree.command(name="thread", description="Create a discussion thread")
@app_commands.describe(topic="Thread topic")
async def create_thread(interaction: discord.Interaction, topic: str):
thread = await interaction.channel.create_thread(
name=topic,
auto_archive_duration=60, # Minutes: 60, 1440, 4320, 10080
)
await thread.send(f"Thread created by {interaction.user.mention}")
await interaction.response.send_message(f"Created thread: {thread.mention}")
# Archive/Close a thread
@tree.command(name="close", description="Archive this thread")
async def close_thread(interaction: discord.Interaction):
if isinstance(interaction.channel, discord.Thread):
await interaction.channel.archive()
await interaction.response.send_message("Thread archived!")
else:
await interaction.response.send_message("This isn't a thread!", ephemeral=True)
```
### Embed Messages
Embeds are rich message blocks commonly used for status updates, logs, and formatted data.
```python
@tree.command(name="info", description="Show project info")
async def project_info(interaction: discord.Interaction):
embed = discord.Embed(
title="Project Dashboard",
description="Current status of all systems",
color=discord.Color.green(),
url="https://github.com/your-repo",
)
embed.set_author(
name="Project Bot",
icon_url="https://example.com/icon.png",
)
embed.add_field(name="Uptime", value="99.9%", inline=True)
embed.add_field(name="Active Users", value="1,234", inline=True)
embed.add_field(name="Issues Open", value="23", inline=False)
embed.set_footer(text="Last updated: just now")
await interaction.response.send_message(embed=embed)
```
## Buttons, Select Menus, and Modals
### Button Interactions
```python
from discord import ui
class ConfirmView(ui.View):
def __init__(self):
super().__init__(timeout=60) # Times out after 60 seconds
self.value = None
@ui.button(label="Confirm", style=discord.ButtonStyle.green)
async def confirm(self, interaction: discord.Interaction, button: ui.Button):
self.value = True
self.stop() # Stops the view
@ui.button(label="Cancel", style=discord.ButtonStyle.red)
async def cancel(self, interaction: discord.Interaction, button: ui.Button):
self.value = False
self.stop()
@tree.command(name="deploy", description="Trigger a deployment")
async def deploy(interaction: discord.Interaction):
view = ConfirmView()
await interaction.response.send_message(
"Are you sure you want to deploy?",
view=view,
)
# Wait for user interaction
await view.wait()
if view.value:
await interaction.edit_original_response(
content="Deploying...",
view=None, # Remove buttons
)
else:
await interaction.edit_original_response(
content="Deployment cancelled.",
view=None,
)
```
### Select Menu (Dropdown)
```python
class RoleSelect(ui.View):
@ui.select(
placeholder="Choose a role...",
options=[
discord.SelectOption(label="Developer", emoji="💻", description="Get developer role"),
discord.SelectOption(label="Designer", emoji="🎨", description="Get designer role"),
discord.SelectOption(label="Manager", emoji="📊", description="Get manager role"),
],
)
async def select_callback(self, interaction: discord.Interaction, select: ui.Select):
# Get the selected role
role_name = select.values[0]
role = discord.utils.get(interaction.guild.roles, name=role_name)
if role:
await interaction.user.add_roles(role)
await interaction.response.send_message(
f"Added {role.mention} to you!",
ephemeral=True,
)
else:
await interaction.response.send_message(
"Role not found!",
ephemeral=True,
)
@tree.command(name="getrole", description="Select a role")
async def getrole(interaction: discord.Interaction):
view = RoleSelect()
await interaction.response.send_message(
"Select a role to assign:",
view=view,
)
```
### Modal Interactions
```python
from discord import ui
import discord
class BugReportModal(ui.Modal, title="Bug Report"):
title_input = ui.TextInput(
label="Bug Title",
placeholder="Brief description",
max_length=100,
)
description_input = ui.TextInput(
label="Steps to Reproduce",
placeholder="How to reproduce this bug...",
style=discord.TextStyle.paragraph,
max_length=1000,
)
severity_input = ui.TextInput(
label="Severity (1-5)",
placeholder="1 = minor, 5 = critical",
max_length=1,
)
async def on_submit(self, interaction: discord.Interaction):
# Log to your system
print(f"Bug: {self.title_input.value}")
print(f"Steps: {self.description_input.value}")
print(f"Severity: {self.severity_input.value}")
await interaction.response.send_message(
"Bug report submitted! Thank you.",
ephemeral=True,
)
@tree.command(name="bug", description="Report a bug")
async def bug_report(interaction: discord.Interaction):
modal = BugReportModal()
await interaction.response.send_modal(modal)
```
## Webhooks
Webhooks allow external services (CI/CD, monitoring) to send messages without a bot user.
### Creating a Webhook
```python
# Via Discord UI: Channel Settings → Integrations → Webhooks
# Or programmatically:
webhook = await channel.create_webhook(name="CI Bot")
print(f"Webhook URL: {webhook.url}")
# Store this URL securely!
```
### Sending Webhook Messages
```python
import aiohttp
WEBHOOK_URL = "https://discord.com/api/webhooks/..."
async def send_webhook(content: str, embed: dict = None):
payload = {"content": content}
if embed:
payload["embeds"] = [embed]
async with aiohttp.ClientSession() as session:
await session.post(WEBHOOK_URL, json=payload)
# Example: CI deployment notification
await send_webhook(
content=f"✅ Deployment complete by <@!{user_id}>",
embed={
"title": "Production Deploy",
"description": "v1.2.3 deployed successfully",
"color": 5763719, # Green
},
)
```
### GitHub-Style Webhook Embed
```python
def github_embed(
title: str,
description: str,
color: int,
fields: list[tuple[str, str, bool]] = None,
) -> dict:
"""Build a GitHub-style embed for webhook notifications."""
embed = {
"title": title,
"description": description,
"color": color,
"footer": {"text": "GitHub Integration"},
"timestamp": datetime.utcnow().isoformat(),
}
if fields:
embed["fields"] = [
{"name": name, "value": value, "inline": inline}
for name, value, inline in fields
]
return embed
```
## Message Components Reference
Discord supports rich message components beyond text:
| Component | Use Case |
|-----------|----------|
| `ui.Button` | Single-click actions |
| `ui.Select` | Dropdown menus |
| `ui.TextInput` | Modal form fields |
| `ui.ChannelSelect` | Select channels |
| `ui.RoleSelect` | Select roles |
| `ui.UserSelect` | Select users |
## Complete Bot Example
```python
import discord
from discord import app_commands
from discord.ext import commands
from datetime import datetime
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
bot = commands.Bot(command_prefix="/", intents=intents)
tree = app_commands.CommandTree(bot)
# ─── Slash Commands ─────────────────────────────────────────────────────────
@tree.command(name="hello", description="Greet the user")
async def hello(interaction: discord.Interaction):
await interaction.response.send_message(
f"Hello {interaction.user.mention}! 👋",
ephemeral=True, # Only visible to user
)
@tree.command(name="server-info", description="Show server information")
async def server_info(interaction: discord.Interaction):
guild = interaction.guild
embed = discord.Embed(
title=guild.name,
color=discord.Color.blue(),
)
embed.add_field(name="Members", value=guild.member_count)
embed.add_field(name="Channels", value=len(guild.channels))
embed.add_field(name="Roles", value=len(guild.roles))
embed.set_thumbnail(url=guild.icon.url if guild.icon else None)
await interaction.response.send_message(embed=embed)
# ─── Message Commands (prefix-based) ─────────────────────────────────────────
@bot.command()
async def ping(ctx: commands.Context):
"""Legacy prefix command - still works alongside slash commands."""
await ctx.send(f"Latency: {round(bot.latency * 1000)}ms")
# ─── Events ──────────────────────────────────────────────────────────────────
@bot.event
async def on_member_join(member: discord.Member):
"""Welcome new members."""
channel = member.guild.system_channel
if channel:
embed = discord.Embed(
title=f"Welcome {member.name}!",
description=f"{member.mention} joined the server.",
color=discord.Color.green(),
)
embed.set_thumbnail(url=member.display_avatar.url)
await channel.send(embed=embed)
@bot.event
async def on_message(message: discord.Message):
"""Handle message events (for prefix commands)."""
if message.author.bot:
return # Ignore bot messages
# Process prefix commands
await bot.process_commands(message)
# ─── Startup ─────────────────────────────────────────────────────────────────
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
print(f"Bot ID: {bot.user.id}")
await tree.sync() # Register all slash commands
print("Slash commands synced!")
bot.run("YOUR_BOT_TOKEN")
```
## REST API vs Gateway
| Approach | Use Case | Library |
|----------|----------|---------|
| **Gateway (WebSocket)** | Real-time bot interactions | discord.py (Client/Bot) |
| **HTTP API (REST)** | One-time actions, webhooks | discord.HTTPClient or requests |
| **Webhook (outbound)** | External services posting to Discord | Just HTTP POST |
Most bot interactions use the Gateway via discord.py. The REST API is for operations like:
```python
# Direct API call example
await bot.http.get_channel(channel_id)
await bot.http.send_message(channel_id, content="Hello via API!")
```
## Rate Limiting
Discord has rate limits on API calls:
- **Global**: ~50-100 requests per second across all endpoints
- **Per-route**: Varies (e.g., sending messages: 5 per 5 seconds per channel)
- **Webhook**: 30 per minute per webhook
discord.py handles rate limiting automatically. For manual HTTP calls:
```python
import asyncio
import aiohttp
async def send_with_retry(url, payload, max_retries=3):
for attempt in range(max_retries):
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as resp:
if resp.status == 429: # Rate limited
retry_after = await resp.json()
await asyncio.sleep(retry_after.get("retry_after", 1))
continue
return await resp.json()
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
raise Exception("Max retries exceeded")
```
## Channel Types Reference
```python
discord.ChannelType.text # Text channel
discord.ChannelType.voice # Voice channel
discord.ChannelType.stage_voice # Stage channel
discord.ChannelType.news # Announcement channel
discord.ChannelType.forum # Forum channel (organized threads)
discord.ChannelType.news_thread # Thread in news channel
discord.ChannelType.public_thread # Public thread
discord.ChannelType.private_thread # Private thread
```
## Role Hierarchy
Discord roles have a hierarchy. Higher roles can manage lower roles:
```python
# Check if user can perform action based on role position
def has_higher_role(user: discord.Member, target: discord.Member) -> bool:
return user.top_role > target.top_role
@tree.command(name="ban", description="Ban a user")
@app_commands.describe(user="User to ban", reason="Reason")
async def ban(interaction: discord.Interaction, user: discord.Member, reason: str = "No reason"):
if not has_higher_role(interaction.user, user):
await interaction.response.send_message(
"You cannot ban someone with a higher or equal role!",
ephemeral=True,
)
return
await interaction.guild.ban(user, reason=reason)
await interaction.response.send_message(f"Banned {user.mention}")
```
## Thread Auto-Archive
Threads automatically archive after inactivity:
| Duration | When to Use |
|----------|-------------|
| 60 minutes | Active discussions |
| 1440 minutes (1 day) | Standard threads |
| 4320 minutes (3 days) | Longer discussions |
| 10080 minutes (1 week) | Slow-moving threads |
```python
# When creating a thread
thread = await channel.create_thread(
name="Discussion",
auto_archive_duration=1440, # 1 day
)
```
## Resources
- [discord.py Documentation](https://discordpy.readthedocs.io/)
- [Discord Developer Portal](https://discord.com/developers/applications)
- [Discord API Documentation](https://discord.com/developers/docs)
- [Discord.js (JavaScript alternative)](https://discord.js.org/)
- [PyNaCl (voice encryption)](https://pynacl.readthedocs.io/)
## Environment Variables Template
Create a `.env` file for your Discord configuration:
```bash
# .env (add to .gitignore!)
DISCORD_BOT_TOKEN=your-bot-token-here
DISCORD_GUILD_ID=123456789012345678
DISCORD_LOG_CHANNEL_ID=123456789012345678
DISCORD_ADMIN_ROLE_ID=123456789012345678
```
## Common Issues
| Issue | Solution |
|-------|----------|
| Bot doesn't respond to slash commands | Ensure `message_content` intent is enabled in Developer Portal |
| Commands not syncing | Call `await tree.sync()` after bot is ready |
| "Application not found" error | Bot token may be wrong or revoked |
| Buttons/selects not working | View must stay alive (don't set `view=None` immediately) |
| Rate limited | Check you're not making too many API calls; use `discord.HTTPClient` |
Built on the Agent Skills standard
Discord Bot Signature Configuration
Cursor IDE rules and conventions for AI-assisted development.
by AgentHub Crawler
webhook
Register webhook endpoints that forward HTTP requests to this chat. Use when user asks to create a webhook, set up an HTTP endpoint, receive API callbacks, integrate external services, listen for incoming requests, or manage existing webhooks (list, delete). Triggers on words like 'webhook', 'endpoint', 'callback', 'HTTP listener', 'API hook', 'incoming request'.
by AgentHub Crawler
DiscoClaw
<!-- KEEP THIS FILE UNDER 3 KB. Details go in .context/*.md modules. --> Personal AI orchestrator — coordinates between Discord, AI runtimes (Claude Code, OpenAI, Codex), and local system resources. Philosophy: small, explicit, auditable. See `docs/philosophy.md`.
by AgentHub Crawler
Sign in to leave a comment.
Python Github Setup Cursorrules Prompt File Rules
Cursor IDE rules and conventions for AI-assisted development.
by AgentHub Crawler