Agent skill
serenity-discord-bot
Discord bot development with Serenity 0.11. Covers event handlers, slash commands, embeds, interactions, TypeMapKey for shared state, and message sending patterns. Use when creating Discord commands, building embeds, handling interactions, or working with the Discord API.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/serenity-discord-bot-ocn-zk-activity
SKILL.md
Serenity Discord Bot Guidelines
Purpose
Development patterns for Discord bots using Serenity 0.11, specifically tailored to this project's architecture.
When to Use
- Creating or modifying slash commands
- Building Discord embeds
- Handling Discord events
- Working with interactions
- Sending messages to channels
- Managing bot state
Project Architecture
Key Files
src/
discord_bot.rs # EventHandler, message sending, embed building
commands.rs # Command trait, helper functions
commands/
subscribe.rs # /subscribe command
unsubscribe.rs # /unsubscribe command
diag.rs # /diag command
...
State Management
The bot uses TypeMapKey to store shared state in Serenity's context:
rust
// In lib.rs
pub struct AppStateContainer;
impl TypeMapKey for AppStateContainer {
type Value = Arc<AppState>;
}
pub struct CommandMap;
impl TypeMapKey for CommandMap {
type Value = Arc<HashMap<String, Box<dyn Command>>>;
}
// Usage in handlers
let data = ctx.data.read().await;
let app_state = data.get::<AppStateContainer>().unwrap();
Event Handler
Basic Structure
rust
use serenity::async_trait;
use serenity::prelude::*;
use serenity::model::gateway::Ready;
use serenity::model::prelude::Interaction;
pub struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
info!("Discord bot {} is connected!", data_about_bot.user.name);
// Register commands here
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
match interaction {
Interaction::ApplicationCommand(command) => {
// Handle slash commands
}
Interaction::MessageComponent(component) => {
// Handle button clicks, select menus
}
_ => {}
}
}
}
Registering Global Commands
rust
async fn ready(&self, ctx: Context, _: Ready) {
let data = ctx.data.read().await;
let command_map = data.get::<CommandMap>().unwrap();
serenity::model::application::command::Command::set_global_application_commands(
&ctx.http,
|commands| {
for cmd in command_map.values() {
commands.create_application_command(|c| cmd.register(c));
}
commands
},
).await.expect("Failed to register commands");
}
Slash Commands
Command Trait Pattern
rust
use serenity::async_trait;
use serenity::builder::CreateApplicationCommand;
use serenity::model::prelude::interaction::application_command::ApplicationCommandInteraction;
#[async_trait]
pub trait Command: Send + Sync {
fn name(&self) -> String;
fn register<'a>(
&self,
command: &'a mut CreateApplicationCommand,
) -> &'a mut CreateApplicationCommand;
async fn execute(
&self,
ctx: &Context,
command: &ApplicationCommandInteraction,
app_state: &Arc<AppState>,
);
}
Implementing a Command
rust
pub struct MyCommand;
#[async_trait]
impl Command for MyCommand {
fn name(&self) -> String {
"mycommand".to_string()
}
fn register<'a>(
&self,
command: &'a mut CreateApplicationCommand,
) -> &'a mut CreateApplicationCommand {
command
.name("mycommand")
.description("Description of my command")
.create_option(|opt| {
opt.name("required_arg")
.description("A required argument")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|opt| {
opt.name("optional_arg")
.description("An optional argument")
.kind(CommandOptionType::Integer)
.required(false)
})
}
async fn execute(
&self,
ctx: &Context,
command: &ApplicationCommandInteraction,
app_state: &Arc<AppState>,
) {
// Extract options
let required = get_option_value(&command.data.options, "required_arg")
.and_then(|v| match v {
CommandDataOptionValue::String(s) => Some(s.clone()),
_ => None,
});
// Respond
command.create_interaction_response(&ctx.http, |r| {
r.interaction_response_data(|m| m.content("Response!"))
}).await.unwrap();
}
}
Helper for Extracting Options
rust
pub fn get_option_value<'a>(
options: &'a [CommandDataOption],
name: &str,
) -> Option<&'a CommandDataOptionValue> {
options
.iter()
.find(|opt| opt.name == name)
.and_then(|opt| opt.resolved.as_ref())
}
Building Embeds
Basic Embed
rust
use serenity::builder::CreateEmbed;
use serenity::utils::Colour;
let mut embed = CreateEmbed::default();
embed.title("Embed Title");
embed.url("https://example.com");
embed.description("Embed description text");
embed.color(Colour::DARK_GREEN);
embed.thumbnail("https://example.com/image.png");
embed.footer(|f| f.text("Footer text"));
embed.timestamp(chrono::Utc::now().to_rfc3339());
Embed with Fields
rust
embed.field("Field Name", "Field value", false); // false = not inline
embed.field("Inline 1", "Value", true);
embed.field("Inline 2", "Value", true);
Embed with Author
rust
embed.author(|a| {
a.name("Author Name")
.url("https://author.link")
.icon_url("https://icon.url")
});
This Project's Embed Pattern
rust
async fn build_killmail_embed(
app_state: &Arc<AppState>,
zk_data: &ZkData,
) -> CreateEmbed {
let mut embed = CreateEmbed::default();
// Build dynamically based on data
embed.title(format!("`{}` destroyed", victim_ship_name));
embed.url(format!("https://zkillboard.com/kill/{}/", kill_id));
embed.author(|a| {
a.name(author_text)
.url(related_br_url)
.icon_url(alliance_icon_url)
});
embed.thumbnail(ship_icon_url);
embed.color(Colour::DARK_GREEN);
embed.field("Attackers", attacker_info, false);
embed.footer(|f| f.text(format!("Value: {}", value_str)));
embed
}
Sending Messages
To a Channel
rust
use serenity::model::prelude::ChannelId;
let channel = ChannelId(channel_id_u64);
channel.send_message(&ctx.http, |m| {
m.content("Message content")
.set_embed(embed)
}).await?;
With Optional Ping
rust
channel.send_message(http, |m| {
if should_ping {
m.content("@everyone")
} else {
m
}
.set_embed(embed)
}).await?;
Responding to Interactions
rust
// Initial response
command.create_interaction_response(&ctx.http, |r| {
r.interaction_response_data(|m| {
m.content("Processing...")
.ephemeral(true) // Only visible to user
})
}).await?;
// Follow-up (can be used multiple times)
command.create_followup_message(&ctx.http, |m| {
m.content("Follow-up message")
}).await?;
// Edit original response
command.edit_original_interaction_response(&ctx.http, |m| {
m.content("Updated response")
}).await?;
Error Handling for Discord Operations
Pattern for Message Sending
rust
pub enum KillmailSendError {
CleanupChannel(serenity::Error), // Channel deleted/no access
Other(Box<dyn std::error::Error + Send + Sync>),
}
pub async fn send_message(/* ... */) -> Result<(), KillmailSendError> {
let result = channel.send_message(http, |m| m.set_embed(embed)).await;
if let Err(e) = result {
if let serenity::Error::Http(http_err) = &e {
match &**http_err {
Error::UnsuccessfulRequest(resp) => match resp.status_code {
StatusCode::FORBIDDEN => {
// Bot removed or no permission
return Err(KillmailSendError::CleanupChannel(e));
}
StatusCode::NOT_FOUND => {
// Channel deleted
return Err(KillmailSendError::CleanupChannel(e));
}
_ => {}
},
_ => {}
}
}
return Err(KillmailSendError::Other(Box::new(e)));
}
Ok(())
}
Component Interactions (Buttons, Select Menus)
Creating a Select Menu
rust
command.create_interaction_response(&ctx.http, |r| {
r.interaction_response_data(|m| {
m.content("Select an option:")
.components(|c| {
c.create_action_row(|row| {
row.create_select_menu(|menu| {
menu.custom_id("my_select_menu")
.placeholder("Choose...")
.options(|opts| {
opts.create_option(|o| {
o.label("Option 1")
.value("opt1")
.description("Description")
})
})
})
})
})
})
}).await?;
Handling Component Interaction
rust
Interaction::MessageComponent(component) => {
let custom_id = &component.data.custom_id;
if custom_id.starts_with("my_select_menu") {
let selected = &component.data.values;
// Handle selection...
component.create_interaction_response(&ctx.http, |r| {
r.interaction_response_data(|m| {
m.content("Selection received!")
.ephemeral(true)
})
}).await?;
}
}
GatewayIntents
rust
let intents = GatewayIntents::non_privileged()
| GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT // Privileged!
| GatewayIntents::GUILD_INTEGRATIONS;
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
.await?;
Quick Reference
| Task | Method |
|---|---|
| Get shared state | ctx.data.read().await.get::<T>() |
| Send to channel | ChannelId(id).send_message(&http, |m| ...) |
| Reply to command | command.create_interaction_response(...) |
| Build embed | CreateEmbed::default() then chain methods |
| Make ephemeral | .ephemeral(true) in response data |
Reference Files
- resources/embed-examples.md - Full embed examples
- resources/command-options.md - All option types
Didn't find tool you were looking for?