Bot Basics¶
Learn the fundamental concepts and patterns for building Discord bots with rustycord.
Overview¶
This guide covers the essential concepts you need to understand when building Discord bots with rustycord, including bot lifecycle, event handling, permissions, and best practices.
Bot Architecture¶
use rustycord::{Bot, Client, MessageHandler, logger};
use rustycord::models::{Message, User};
use rustycord::gateway::intents::Intents;
use async_trait::async_trait;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup logging
logger::setup_logger("info".to_string())?;
// Create bot with specific intents
let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT;
let mut bot = Bot::new(Some(intents.bits() as i32)).await;
// Login
let token = std::env::var("DISCORD_TOKEN")?;
bot.login(token).await;
// Register handlers
bot.register_message_handler(Box::new(BasicCommandHandler));
bot.register_message_handler(Box::new(AdminHandler::new()));
// Start the bot
bot.start().await?;
Ok(())
}
Bot Lifecycle¶
1. Initialization¶
// Create bot instance
let mut bot = Bot::new(None).await;
// Configure bot settings
bot.set_activity("Watching for commands");
bot.set_status("online");
2. Authentication¶
// Login with bot token
let user_response = bot.login(token).await;
log::info!("Bot logged in as: {}", user_response.username);
3. Handler Registration¶
// Register message handlers
bot.register_message_handler(Box::new(CommandHandler));
bot.register_message_handler(Box::new(ModerationHandler));
// Register event handlers (future feature)
// bot.register_event_handler(Box::new(GuildHandler));
4. Event Loop¶
Message Handling Patterns¶
Basic Command Handler¶
struct BasicCommandHandler;
#[async_trait]
impl MessageHandler for BasicCommandHandler {
async fn handle_message(&self, message: &Message) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
// Ignore bot messages
if message.author.bot.unwrap_or(false) {
return Ok(None);
}
// Parse command
let content = message.content.trim();
if !content.starts_with('!') {
return Ok(None);
}
let parts: Vec<&str> = content[1..].split_whitespace().collect();
if parts.is_empty() {
return Ok(None);
}
let command = parts[0].to_lowercase();
let args = &parts[1..];
match command.as_str() {
"ping" => Ok(Some("🏓 Pong!".to_string())),
"help" => Ok(Some(self.get_help_text())),
"echo" => {
if args.is_empty() {
Ok(Some("❌ Please provide text to echo.".to_string()))
} else {
Ok(Some(args.join(" ")))
}
},
"info" => Ok(Some(self.get_bot_info())),
_ => Ok(None), // Command not handled by this handler
}
}
}
impl BasicCommandHandler {
fn get_help_text(&self) -> String {
"📚 **Available Commands**\n\
`!ping` - Test bot responsiveness\n\
`!help` - Show this help message\n\
`!echo <text>` - Echo your message\n\
`!info` - Show bot information".to_string()
}
fn get_bot_info(&self) -> String {
"🤖 **Bot Information**\n\
Name: RustyCord Bot\n\
Version: 0.1.1\n\
Language: Rust\n\
Library: RustyCord".to_string()
}
}
Advanced Command Handler with Subcommands¶
use std::collections::HashMap;
struct AdvancedCommandHandler {
commands: HashMap<String, Box<dyn Command + Send + Sync>>,
}
#[async_trait]
trait Command {
async fn execute(&self, args: &[&str], message: &Message) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
fn description(&self) -> &str;
fn usage(&self) -> &str;
}
impl AdvancedCommandHandler {
fn new() -> Self {
let mut commands: HashMap<String, Box<dyn Command + Send + Sync>> = HashMap::new();
commands.insert("user".to_string(), Box::new(UserCommand));
commands.insert("server".to_string(), Box::new(ServerCommand));
commands.insert("math".to_string(), Box::new(MathCommand));
Self { commands }
}
}
#[async_trait]
impl MessageHandler for AdvancedCommandHandler {
async fn handle_message(&self, message: &Message) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if !message.content.starts_with('!') {
return Ok(None);
}
let parts: Vec<&str> = message.content[1..].split_whitespace().collect();
if parts.is_empty() {
return Ok(None);
}
let command_name = parts[0].to_lowercase();
let args = &parts[1..];
if let Some(command) = self.commands.get(&command_name) {
match command.execute(args, message).await {
Ok(response) => Ok(Some(response)),
Err(e) => Ok(Some(format!("❌ Error executing command: {}", e))),
}
} else if command_name == "help" {
Ok(Some(self.generate_help()))
} else {
Ok(None)
}
}
}
impl AdvancedCommandHandler {
fn generate_help(&self) -> String {
let mut help = "📚 **Available Commands**\n".to_string();
for (name, command) in &self.commands {
help.push_str(&format!(
"`!{} {}` - {}\n",
name,
command.usage(),
command.description()
));
}
help.push_str("`!help` - Show this help message");
help
}
}
// Command implementations
struct UserCommand;
#[async_trait]
impl Command for UserCommand {
async fn execute(&self, args: &[&str], message: &Message) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
match args.get(0) {
Some(&"info") => {
Ok(format!(
"👤 **User Information**\n\
Username: {}\n\
User ID: {}\n\
Account Type: {}",
message.author.username,
message.author.id,
if message.author.bot.unwrap_or(false) { "Bot" } else { "User" }
))
},
Some(&"avatar") => {
Ok(format!(
"🖼️ **User Avatar**\n\
Username: {}\n\
Avatar URL: {}",
message.author.username,
message.author.avatar_url().unwrap_or("No avatar".to_string())
))
},
_ => Ok("❓ Available subcommands: `info`, `avatar`".to_string()),
}
}
fn description(&self) -> &str {
"User-related commands"
}
fn usage(&self) -> &str {
"<info|avatar>"
}
}
struct ServerCommand;
#[async_trait]
impl Command for ServerCommand {
async fn execute(&self, args: &[&str], message: &Message) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
match args.get(0) {
Some(&"info") => {
Ok(format!(
"🏠 **Server Information**\n\
Guild ID: {}\n\
Channel ID: {}",
message.guild_id.unwrap_or_default(),
message.channel_id
))
},
_ => Ok("❓ Available subcommands: `info`".to_string()),
}
}
fn description(&self) -> &str {
"Server-related commands"
}
fn usage(&self) -> &str {
"<info>"
}
}
struct MathCommand;
#[async_trait]
impl Command for MathCommand {
async fn execute(&self, args: &[&str], message: &Message) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if args.len() != 3 {
return Ok("❓ Usage: `!math <number> <operator> <number>`".to_string());
}
let a: f64 = args[0].parse().map_err(|_| "Invalid first number")?;
let op = args[1];
let b: f64 = args[2].parse().map_err(|_| "Invalid second number")?;
let result = match op {
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" => {
if b == 0.0 {
return Ok("❌ Division by zero".to_string());
}
a / b
},
"%" => a % b,
"^" => a.powf(b),
_ => return Ok("❌ Supported operators: +, -, *, /, %, ^".to_string()),
};
Ok(format!("🧮 {} {} {} = {}", a, op, b, result))
}
fn description(&self) -> &str {
"Basic math operations"
}
fn usage(&self) -> &str {
"<number> <operator> <number>"
}
}
Permission Handling¶
use rustycord::models::{User, Role, Permission};
struct PermissionChecker;
impl PermissionChecker {
fn has_permission(user: &User, required_permission: Permission) -> bool {
// In a real implementation, you'd check Discord permissions
// This is a simplified example
user.roles.iter().any(|role| role.permissions.contains(required_permission))
}
fn is_admin(user: &User) -> bool {
Self::has_permission(user, Permission::ADMINISTRATOR)
}
fn is_moderator(user: &User) -> bool {
Self::has_permission(user, Permission::MANAGE_MESSAGES) ||
Self::has_permission(user, Permission::KICK_MEMBERS) ||
Self::is_admin(user)
}
fn can_manage_roles(user: &User) -> bool {
Self::has_permission(user, Permission::MANAGE_ROLES) || Self::is_admin(user)
}
}
// Usage in command handler
struct AdminHandler;
#[async_trait]
impl MessageHandler for AdminHandler {
async fn handle_message(&self, message: &Message) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if !message.content.starts_with("!admin ") {
return Ok(None);
}
// Check if user has admin permissions
if !PermissionChecker::is_admin(&message.author) {
return Ok(Some("❌ You don't have permission to use admin commands.".to_string()));
}
let command = message.content.strip_prefix("!admin ").unwrap_or("");
match command {
"shutdown" => {
log::warn!("🛑 Admin {} requested bot shutdown", message.author.username);
Ok(Some("🛑 Shutting down bot...".to_string()))
},
"restart" => {
log::info!("🔄 Admin {} requested bot restart", message.author.username);
Ok(Some("🔄 Restarting bot...".to_string()))
},
"stats" => {
Ok(Some(self.get_bot_stats()))
},
_ => Ok(Some("❓ Available admin commands: shutdown, restart, stats".to_string())),
}
}
}
impl AdminHandler {
fn get_bot_stats(&self) -> String {
"📊 **Bot Statistics**\n\
Uptime: 2 hours, 34 minutes\n\
Messages Processed: 1,247\n\
Commands Executed: 89\n\
Memory Usage: 45.2 MB\n\
Active Connections: 1".to_string()
}
}
Error Handling¶
use anyhow::{Context, Result};
#[async_trait]
impl MessageHandler for ErrorHandlingHandler {
async fn handle_message(&self, message: &Message) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> {
if !message.content.starts_with("!test") {
return Ok(None);
}
match self.process_test_command(message).await {
Ok(response) => Ok(Some(response)),
Err(e) => {
log::error!("❌ Error processing test command: {:#}", e);
Ok(Some("❌ An error occurred while processing your command.".to_string()))
}
}
}
}
impl ErrorHandlingHandler {
async fn process_test_command(&self, message: &Message) -> Result<String> {
// Simulate potential failure points
self.validate_user(message)
.context("User validation failed")?;
let data = self.fetch_data()
.await
.context("Failed to fetch required data")?;
let processed = self.process_data(data)
.context("Data processing failed")?;
Ok(format!("✅ Command completed: {}", processed))
}
fn validate_user(&self, message: &Message) -> Result<()> {
if message.author.username.is_empty() {
anyhow::bail!("Username cannot be empty");
}
Ok(())
}
async fn fetch_data(&self) -> Result<String> {
// Simulate async operation that might fail
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
Ok("sample_data".to_string())
}
fn process_data(&self, data: String) -> Result<String> {
if data.is_empty() {
anyhow::bail!("Cannot process empty data");
}
Ok(format!("processed_{}", data))
}
}
Best Practices¶
1. Handler Organization¶
// Organize handlers by functionality
mod handlers {
pub mod commands;
pub mod moderation;
pub mod utility;
pub mod fun;
}
// Register handlers in a organized way
fn register_handlers(bot: &mut Bot) {
// Core functionality
bot.register_message_handler(Box::new(handlers::commands::BasicCommands));
bot.register_message_handler(Box::new(handlers::utility::UtilityCommands));
// Moderation (order matters - check permissions first)
bot.register_message_handler(Box::new(handlers::moderation::ModerationCommands));
// Fun commands (last, as they might be catch-all)
bot.register_message_handler(Box::new(handlers::fun::FunCommands));
}
2. Configuration Management¶
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct BotConfig {
pub token: String,
pub prefix: String,
pub log_level: String,
pub owner_id: String,
pub admin_roles: Vec<String>,
pub disabled_commands: Vec<String>,
}
impl BotConfig {
fn load() -> Result<Self, Box<dyn std::error::Error>> {
let config_str = std::fs::read_to_string("config.toml")?;
let config: BotConfig = toml::from_str(&config_str)?;
Ok(config)
}
}
3. Rate Limiting¶
use std::collections::HashMap;
use std::time::{Duration, Instant};
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct RateLimiter {
user_last_command: Arc<Mutex<HashMap<String, Instant>>>,
cooldown: Duration,
}
impl RateLimiter {
fn new(cooldown_seconds: u64) -> Self {
Self {
user_last_command: Arc::new(Mutex::new(HashMap::new())),
cooldown: Duration::from_secs(cooldown_seconds),
}
}
fn check_rate_limit(&self, user_id: &str) -> bool {
let mut last_commands = self.user_last_command.lock().unwrap();
let now = Instant::now();
if let Some(last_time) = last_commands.get(user_id) {
if now.duration_since(*last_time) < self.cooldown {
return false; // Rate limited
}
}
last_commands.insert(user_id.to_string(), now);
true // Allow command
}
}
Testing¶
#[cfg(test)]
mod tests {
use super::*;
use rustycord::models::{User, Message};
fn create_test_message(content: &str, username: &str) -> Message {
Message {
content: content.to_string(),
author: User {
id: "123456789".to_string(),
username: username.to_string(),
bot: Some(false),
// ... other fields
},
// ... other fields
}
}
#[tokio::test]
async fn test_basic_command_handler() {
let handler = BasicCommandHandler;
let message = create_test_message("!ping", "testuser");
let result = handler.handle_message(&message).await.unwrap();
assert_eq!(result, Some("🏓 Pong!".to_string()));
}
#[tokio::test]
async fn test_echo_command() {
let handler = BasicCommandHandler;
let message = create_test_message("!echo Hello World", "testuser");
let result = handler.handle_message(&message).await.unwrap();
assert_eq!(result, Some("Hello World".to_string()));
}
#[tokio::test]
async fn test_non_command_message() {
let handler = BasicCommandHandler;
let message = create_test_message("Hello there", "testuser");
let result = handler.handle_message(&message).await.unwrap();
assert_eq!(result, None);
}
}
Related Documentation¶
- Installation Guide - Set up your development environment
- Prefix Commands - Advanced command patterns
- Examples - Working code examples