| name | bot-module-development |
| description | Guide for creating new protection modules in the Open Guard Discord bot Use when this capability is needed. |
| metadata | {"author":"erdemgksl"} |
Bot Module Development
What this skill does
Provides guidance on creating new protection modules, including commands, event handlers, and database entities in the Open Guard bot.
When to use
Use this skill when you need to:
- Create a new protection or utility module
- Add new slash commands to existing modules
- Implement event handlers for Discord events
- Add database entities and migrations
- Configure module settings
⚠️ CRITICAL: Do Not Await Interactions in Commands
NEVER await button/select/menu interactions in command handlers. This is a hard rule in Open Guard.
Why?
- Commands must return immediately
- Awaiting interactions blocks the command handler
- Interaction handlers are separate and asynchronous
- Data should be transported via
custom_id parsing
The Pattern
#[poise::command(slash_command)]
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.components(vec![
serenity::CreateButton::new("btn_confirm")
.label("Confirm")
.style(serenity::ButtonStyle::Success),
])
).await?;
let interaction = ctx.channel_id().await_component(&ctx).await?;
Ok(())
}
#[poise::command(slash_command)]
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
let setup_id = generate_unique_id();
ctx.send(poise::CreateReply::default()
.content("Confirm the action")
.components(vec![
serenity::CreateButton::new(format!("btn_confirm_{}", setup_id))
.label("Confirm")
.style(serenity::ButtonStyle::Success),
])
).await?;
Ok(())
}
async fn handle_component_interaction(
ctx: &serenity::Context,
interaction: &serenity::ComponentInteraction,
data: &Data,
) -> Result<(), Error> {
let custom_id = &interaction.data.custom_id;
if let Some(setup_id) = custom_id.strip_prefix("btn_confirm_") {
println!("Confirming action for setup: {}", setup_id);
interaction.create_response(
ctx,
serenity::CreateInteractionResponse::UpdateMessage(
serenity::CreateInteractionResponseMessage::new()
.content("Action confirmed!")
.components(vec![])
)
).await?;
}
Ok(())
}
Module Structure
A module consists of three main parts:
- Commands - Slash commands for user interaction
- Event Handlers - Functions that respond to Discord events
- Module Definition - Metadata and registration
Basic Module Layout
src/modules/my_module/
├── mod.rs # Module definition and registration
├── commands/ # Slash commands
│ └── mod.rs
└── events/ # Event handlers
└── mod.rs
Creating a New Module
Step 1: Create Module Directory Structure
mkdir -p src/modules/my_module/commands src/modules/my_module/events
Step 2: Create Module Definition (mod.rs)
pub mod commands;
pub mod events;
use crate::modules::{Module, ModuleDefinition};
pub fn module() -> Module {
Module {
definition: ModuleDefinition {
id: "my_module",
name_key: "module-my-module-name",
desc_key: "module-my-module-desc",
},
commands: vec![
commands::my_command(),
],
event_handlers: vec![events::handler],
}
}
Step 3: Register the Module
Add your module to src/modules/mod.rs:
pub mod my_module;
pub fn get_modules() -> Vec<Module> {
vec![
my_module::module(),
]
}
Step 4: Add Module Type to Database
Update src/db/entities/module_configs.rs to include your module:
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "module_type")]
pub enum ModuleType {
#[sea_orm(string_value = "MyModule")]
MyModule,
}
pub fn module_type_from_string(s: &str) -> Option<ModuleType> {
match s {
"MyModule" => Some(ModuleType::MyModule),
_ => None,
}
}
Creating Commands
Basic Command Structure
use crate::{Context, Error};
use poise::serenity_prelude as serenity;
#[poise::command(
slash_command,
guild_only,
required_permissions = "ADMINISTRATOR",
ephemeral // Makes response ephemeral (only user sees it)
)]
pub async fn my_command(
ctx: Context<'_>,
#[description = "User to target"] user: serenity::User,
#[description = "Optional reason"] reason: Option<String>,
) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
let guild_id = ctx.guild_id().unwrap();
let l10n = ctx.l10n_user();
ctx.send(
poise::CreateReply::default()
.content("Command executed successfully!")
)
.await?;
Ok(())
}
⚠️ Components and Interaction Handling
When adding buttons, selects, or modals to your commands:
pub async fn interactive_command(ctx: Context<'_>) -> Result<(), Error> {
let setup_id = generate_unique_id();
ctx.send(poise::CreateReply::default()
.components(vec![
serenity::CreateButton::new(format!("btn_confirm_{}", setup_id))
.label("Confirm")
.style(serenity::ButtonStyle::Success),
serenity::CreateButton::new(format!("btn_cancel_{}", setup_id))
.label("Cancel")
.style(serenity::ButtonStyle::Danger),
])
).await?;
Ok(())
}
Using Database Entities
use crate::db::entities::module_configs::ModuleType;
use sea_orm::{ActiveModelTrait, EntityTrait, QueryFilter, Set};
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let db = &ctx.data().db;
let config = crate::db::entities::module_configs::Entity::find()
.filter(crate::db::entities::module_configs::Column::GuildId.eq(guild_id.get() as i64))
.filter(crate::db::entities::module_configs::Column::ModuleType.eq(ModuleType::MyModule))
.one(db)
.await?;
if !config.map(|c| c.enabled).unwrap_or(false) {
ctx.send(poise::CreateReply::default()
.content("Module is not enabled for this server!")
).await?;
return Ok(());
}
Ok(())
}
Subcommands
#[poise::command(
slash_command,
subcommands("add", "remove", "list")
)]
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Use a subcommand: /my_command add, /my_command remove, /my_command list")
).await?;
Ok(())
}
#[poise::command(slash_command)]
pub async fn add(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
#[poise::command(slash_command)]
pub async fn remove(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
#[poise::command(slash_command)]
pub async fn list(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
Creating Event Handlers
Basic Event Handler
use crate::modules::EventHandler;
use crate::{Data, Error};
use poise::serenity_prelude as serenity;
pub async fn handler(
ctx: &serenity::Context,
event: &serenity::FullEvent,
data: &Data,
) -> Result<(), Error> {
match event {
serenity::FullEvent::Message { new_message } => {
handle_message(ctx, new_message, data).await?;
}
serenity::FullEvent::GuildMemberAddition { new_member } => {
handle_member_add(ctx, new_member, data).await?;
}
serenity::FullEvent::AuditLogEntryCreate { entry, .. } => {
handle_audit_log(ctx, entry, data).await?;
}
_ => {}
}
Ok(())
}
async fn handle_message(
ctx: &serenity::Context,
message: &serenity::Message,
data: &Data,
) -> Result<(), Error> {
if let Some(guild_id) = message.guild_id {
let enabled = is_module_enabled(guild_id.get(), &data.db).await?;
if !enabled {
return Ok(());
}
}
(())
}
(guild_id: , db: &sea_orm::DatabaseConnection) <, Error> {
crate::db::entities::module_configs;
sea_orm::{EntityTrait, QueryFilter};
= module_configs::Entity::()
.(module_configs::Column::GuildId.(guild_id ))
.(module_configs::Column::ModuleType.(module_configs::ModuleType::MyModule))
.(db)
.?;
(config.(|c| c.enabled).())
}
Available Event Types
Common Discord events you can handle:
Message { new_message } - New messages
MessageUpdate { old_if_available, new, event } - Message edits
MessageDelete { channel_id, deleted_message_id, guild_id } - Message deletions
GuildMemberAddition { new_member } - Members joining
GuildMemberRemoval { guild_id, user, member_data_if_available } - Members leaving
GuildMemberUpdate { old_if_available, new } - Member updates
AuditLogEntryCreate { entry, guild_id } - Audit log entries
VoiceStateUpdate { old, new } - Voice state changes
ChannelCreate { channel } - Channel creation
ChannelDelete { channel } - Channel deletion
RoleCreate { role } - Role creation
RoleDelete { role_id, guild_id } - Role deletion
Localization
Add localization keys to your locale files (in locales/):
module-my-module-name = My Module
module-my-module-desc = Description of what this module does
my-command-name = My Command
my-command-desc = Description of the command
my-command-success = Command executed successfully!
my-command-error = Failed to execute command: { $error }
Use localization in your commands:
use fluent::FluentArgs;
let l10n = ctx.l10n_user();
let mut args = FluentArgs::new();
args.set("error", error_message);
ctx.send(poise::CreateReply::default()
.content(l10n.t("my-command-error", Some(&args)))
).await?;
Logging
Use the built-in logger service:
use crate::services::logger::LogLevel;
use crate::db::entities::module_configs::ModuleType;
ctx.data().logger.log_context(
&ctx,
Some(ModuleType::MyModule),
LogLevel::Audit,
&l10n_guild.t("log-my-command-title", None),
&l10n_guild.t("log-my-command-desc", Some(&log_args)),
vec![
(&l10n_guild.t("log-field-user", None), format!("<@{}>", user.id)),
(&l10n_guild.t("log-field-reason", None), reason),
],
).await?;
Best Practices
- ⚠️ NEVER await interactions in commands: Commands must return immediately
- ⚠️ ALWAYS use separate handlers: Handle interactions in event handlers, not commands
- ⚠️ ALWAYS use custom_id parsing: Transport data via custom_ids, not awaiting
- Check module enablement: Always check if the module is enabled before performing actions
- Use localization: Make all user-facing text translatable
- Handle errors gracefully: Provide meaningful error messages to users
- Log important actions: Use the logger service for audit trails
- Use
defer_ephemeral(): For long-running commands to provide immediate feedback
- Validate permissions: Ensure users have required permissions before actions
- Use transactions: When making multiple database changes, wrap in a transaction
Testing Your Module
- Build the project:
cargo build --release
- Run the bot:
cargo run --release
- Register commands (first time):
cargo run --release -- --publish GUILD_ID
- Test commands in Discord
- Check logs for errors
- Test interaction handlers (buttons, selects, modals)
Examples
See existing modules for reference:
src/modules/moderation_protection/ - Moderation commands
src/modules/logging/ - Logging events
src/modules/role_protection/ - Role protection events
src/services/setup/mod.rs - Complete example of multi-step interactions with custom_id parsing
Converted and distributed by TomeVault — claim your Tome and manage your conversions.