Claude-skill-registry discord-bot
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/discord-bot" ~/.claude/skills/majiayu000-claude-skill-registry-discord-bot && rm -rf "$T"
manifest:
skills/data/discord-bot/SKILL.mdsource content
Discord Bot Development
Project Protection Setup
MANDATORY before writing any code:
# 1. Create .gitignore cat >> .gitignore << 'EOF' # Build target/ node_modules/ __pycache__/ dist/ # Secrets - CRITICAL for bots! .env .env.* !.env.example bot_token.txt config.json # If contains token # IDE .idea/ .vscode/ .DS_Store EOF # 2. Setup pre-commit hooks cat > .pre-commit-config.yaml << 'EOF' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: detect-private-key - id: check-added-large-files - repo: https://github.com/gitleaks/gitleaks rev: v8.21.2 hooks: - id: gitleaks EOF pre-commit install
Why critical: Discord bot tokens give FULL access. Leaked token = bot compromised, can spam users.
Stack Options
| Language | Framework | Best For |
|---|---|---|
| Rust | serenity + poise | Performance, type safety |
| Python | discord.py / nextcord | Rapid development |
| Node | discord.js | JS ecosystem, largest community |
Quick Start
Rust (serenity + poise)
# Cargo.toml [dependencies] serenity = { version = "0.12", features = ["framework"] } poise = "0.6" tokio = { version = "1", features = ["full"] }
use poise::serenity_prelude as serenity; struct Data {} type Error = Box<dyn std::error::Error + Send + Sync>; type Context<'a> = poise::Context<'a, Data, Error>; /// Say hello #[poise::command(slash_command)] async fn hello(ctx: Context<'_>) -> Result<(), Error> { ctx.say("Hello!").await?; Ok(()) } #[tokio::main] async fn main() { let framework = poise::Framework::builder() .options(poise::FrameworkOptions { commands: vec![hello()], ..Default::default() }) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data {}) }) }) .build(); let token = std::env::var("DISCORD_TOKEN").unwrap(); let intents = serenity::GatewayIntents::non_privileged(); let client = serenity::ClientBuilder::new(token, intents) .framework(framework) .await .unwrap(); client.start().await.unwrap(); }
Python (discord.py)
# requirements.txt discord.py>=2.0
import discord from discord import app_commands intents = discord.Intents.default() client = discord.Client(intents=intents) tree = app_commands.CommandTree(client) @tree.command(name="hello", description="Say hello") async def hello(interaction: discord.Interaction): await interaction.response.send_message("Hello!") @client.event async def on_ready(): await tree.sync() print(f"Logged in as {client.user}") client.run("BOT_TOKEN")
Node (discord.js)
// package.json: "discord.js": "^14.14" import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes } from 'discord.js'; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const commands = [ new SlashCommandBuilder().setName('hello').setDescription('Say hello'), ].map(cmd => cmd.toJSON()); client.once('ready', async () => { const rest = new REST().setToken(process.env.DISCORD_TOKEN!); await rest.put(Routes.applicationCommands(client.user!.id), { body: commands }); console.log(`Logged in as ${client.user?.tag}`); }); client.on('interactionCreate', async (interaction) => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === 'hello') { await interaction.reply('Hello!'); } }); client.login(process.env.DISCORD_TOKEN);
Slash Commands
With Options (Rust/poise)
/// Ban a user #[poise::command(slash_command, required_permissions = "BAN_MEMBERS")] async fn ban( ctx: Context<'_>, #[description = "User to ban"] user: serenity::User, #[description = "Reason"] reason: Option<String>, ) -> Result<(), Error> { let reason = reason.unwrap_or_else(|| "No reason provided".to_string()); ctx.guild_id() .unwrap() .ban_with_reason(&ctx.serenity_context().http, user.id, 0, &reason) .await?; ctx.say(format!("Banned {} for: {}", user.name, reason)).await?; Ok(()) }
With Options (Python)
@tree.command(name="ban", description="Ban a user") @app_commands.describe(user="User to ban", reason="Reason for ban") @app_commands.default_permissions(ban_members=True) async def ban( interaction: discord.Interaction, user: discord.Member, reason: str = "No reason provided" ): await user.ban(reason=reason) await interaction.response.send_message(f"Banned {user.name} for: {reason}")
Embeds
Rust
use serenity::builder::{CreateEmbed, CreateMessage}; let embed = CreateEmbed::new() .title("User Info") .description("Details about the user") .field("Username", &user.name, true) .field("ID", user.id.to_string(), true) .color(0x00ff00) .thumbnail(user.avatar_url().unwrap_or_default()); ctx.send(poise::CreateReply::default().embed(embed)).await?;
Python
embed = discord.Embed( title="User Info", description="Details about the user", color=0x00ff00 ) embed.add_field(name="Username", value=user.name, inline=True) embed.add_field(name="ID", value=str(user.id), inline=True) embed.set_thumbnail(url=user.avatar.url if user.avatar else None) await interaction.response.send_message(embed=embed)
Buttons & Components
Rust
use serenity::builder::{CreateButton, CreateActionRow}; let button = CreateButton::new("confirm") .label("Confirm") .style(serenity::ButtonStyle::Primary); let cancel = CreateButton::new("cancel") .label("Cancel") .style(serenity::ButtonStyle::Danger); let row = CreateActionRow::Buttons(vec![button, cancel]); ctx.send(poise::CreateReply::default() .content("Are you sure?") .components(vec![row]) ).await?;
Python
from discord.ui import Button, View class ConfirmView(View): @discord.ui.button(label="Confirm", style=discord.ButtonStyle.primary) async def confirm(self, interaction: discord.Interaction, button: Button): await interaction.response.send_message("Confirmed!") self.stop() @discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger) async def cancel(self, interaction: discord.Interaction, button: Button): await interaction.response.send_message("Cancelled!") self.stop() view = ConfirmView() await interaction.response.send_message("Are you sure?", view=view)
Event Handlers
Rust
struct Handler; #[serenity::async_trait] impl serenity::EventHandler for Handler { async fn message(&self, ctx: serenity::Context, msg: serenity::Message) { if msg.content == "!ping" { msg.channel_id.say(&ctx.http, "Pong!").await.ok(); } } async fn guild_member_addition(&self, ctx: serenity::Context, member: serenity::Member) { if let Some(channel) = member.guild_id.to_guild_cached(&ctx.cache) { // Send welcome message } } }
Python
@client.event async def on_message(message: discord.Message): if message.author.bot: return if message.content == "!ping": await message.channel.send("Pong!") @client.event async def on_member_join(member: discord.Member): channel = member.guild.system_channel if channel: await channel.send(f"Welcome {member.mention}!")
Permissions
Rust
#[poise::command( slash_command, required_permissions = "MANAGE_MESSAGES", // Bot needs this default_member_permissions = "MANAGE_MESSAGES", // User needs this )] async fn clear( ctx: Context<'_>, #[description = "Number of messages"] count: u8, ) -> Result<(), Error> { let messages = ctx.channel_id() .messages(&ctx.serenity_context().http, serenity::GetMessages::new().limit(count)) .await?; ctx.channel_id() .delete_messages(&ctx.serenity_context().http, messages) .await?; ctx.say(format!("Deleted {} messages", count)).await?; Ok(()) }
Sharding (for 2500+ servers)
Rust
let client = serenity::ClientBuilder::new(token, intents) .framework(framework) .await?; // Auto-sharding client.start_autosharded().await?; // Or manual sharding // client.start_shard(shard_id, total_shards).await?;
Python
from discord import AutoShardedClient client = AutoShardedClient(intents=intents)
Environment & Security
# .env DISCORD_TOKEN=your_bot_token GUILD_ID=123456789 # For development # .gitignore .env
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Commands not showing | Call / register commands |
| Missing intents | Enable in Developer Portal + code |
| Rate limits | Use caching, batch operations |
| Privileged intents | Enable in portal for members/presence |
| Token exposed | Use env vars, never commit |
Bot Permissions Calculator
Required intents for common features:
- Messages:
(privileged)MESSAGE_CONTENT - Members:
(privileged)GUILD_MEMBERS - Presence:
(privileged)GUILD_PRESENCES
Invite URL format:
https://discord.com/api/oauth2/authorize?client_id=YOUR_ID&permissions=PERMS&scope=bot%20applications.commands
Testing
Rust (serenity/poise)
#[cfg(test)] mod tests { use super::*; #[test] fn test_parse_duration() { assert_eq!(parse_duration("1h"), Duration::hours(1)); assert_eq!(parse_duration("30m"), Duration::minutes(30)); } #[tokio::test] async fn test_ban_requires_permission() { // Test permission checks let result = check_ban_permission(user_without_perms).await; assert!(result.is_err()); } }
Python (discord.py)
import pytest from unittest.mock import AsyncMock, MagicMock @pytest.fixture def mock_interaction(): interaction = MagicMock() interaction.response.send_message = AsyncMock() interaction.user.guild_permissions.ban_members = True return interaction @pytest.mark.asyncio async def test_hello_command(mock_interaction): await hello(mock_interaction) mock_interaction.response.send_message.assert_called_once() @pytest.mark.asyncio async def test_ban_without_permission(mock_interaction): mock_interaction.user.guild_permissions.ban_members = False with pytest.raises(discord.errors.Forbidden): await ban(mock_interaction, MagicMock())
Node (discord.js)
import { describe, it, expect, vi } from 'vitest'; describe('Commands', () => { it('hello command replies', async () => { const interaction = { reply: vi.fn(), isChatInputCommand: () => true, commandName: 'hello', }; await handleInteraction(interaction); expect(interaction.reply).toHaveBeenCalled(); }); });
TDD Workflow
1. Task[tdd-test-writer]: "Create /ban slash command" → Writes test expecting permission check + success → cargo test / pytest / npm test → FAILS (RED) 2. Task[rust-developer]: "Implement /ban command" → Implements with permission checks → Tests PASS (GREEN) 3. Repeat for each command 4. Task[code-reviewer]: "Review bot implementation" → Checks security, permissions, rate limits
Security Checklist
- Token in environment variable (never in code)
-
in.env.gitignore - pre-commit hooks with gitleaks
- Permission checks on all moderation commands
- Rate limiting for user commands
- Input sanitization (no injection in embeds)
- No privileged intents unless needed
- Audit log for moderation actions
Project Structure
discord-bot/ ├── src/ │ ├── main.rs │ ├── commands/ │ │ ├── mod.rs │ │ ├── moderation.rs │ │ └── fun.rs │ └── events.rs ├── tests/ │ └── commands_test.rs ├── Cargo.toml ├── .env.example ├── .env # NOT committed └── .gitignore