Poise Commands
What this skill does
Provides comprehensive guidance on creating Discord slash commands using the Poise framework in the Open Guard bot.
When to use
Use this skill when you need to:
- •Create new slash commands
- •Add command parameters and options
- •Handle command responses
- •Work with command contexts and data
- •Implement subcommands and command groups
⚠️ 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_idparsing
The Pattern
rust
// ❌ WRONG: Awaiting interaction in command
#[poise::command(slash_command)]
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.components(vec![
serenity::CreateButton::new("btn_click")
.label("Click Me!")
.style(serenity::ButtonStyle::Primary),
])
).await?;
// WRONG: This blocks the command!
let msg = ctx.channel_id().await_message(&ctx).await?;
Ok(())
}
// ✅ CORRECT: Send and return immediately
#[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("Click the button!")
.components(vec![
serenity::CreateButton::new(format!("btn_click_{}", setup_id))
.label("Click Me!")
.style(serenity::ButtonStyle::Primary),
])
).await?;
// ✅ Command returns immediately
Ok(())
}
// Handle interaction in separate event handler (serenity-interactions skill)
Basic Command Structure
Simple Command
rust
use crate::{Context, Error};
use poise::serenity_prelude as serenity;
/// A simple greeting command
#[poise::command(
slash_command,
guild_only,
ephemeral // Only the user who ran the command sees the response
)]
pub async fn hello(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Hello, World!")
).await?;
Ok(())
}
Command with Parameters
rust
/// Say hello to someone
#[poise::command(
slash_command,
guild_only,
ephemeral
)]
pub async fn greet(
ctx: Context<'_>,
#[description = "The user to greet"] user: serenity::User,
#[description = "Optional greeting message"] message: Option<String>,
) -> Result<(), Error> {
let greeting = message.unwrap_or_else(|| "Hello".to_string());
ctx.send(poise::CreateReply::default()
.content(format!("{} {}!", greeting, user.name))
).await?;
Ok(())
}
Command Options
Required Permissions
rust
#[poise::command(
slash_command,
guild_only,
required_permissions = "BAN_MEMBERS" // Only users with BAN_MEMBERS can use
)]
pub async fn ban_command(ctx: Context<'_>) -> Result<(), Error> {
// Command logic
Ok(())
}
Available permissions: ADMINISTRATOR, BAN_MEMBERS, KICK_MEMBERS, MANAGE_MESSAGES, MANAGE_GUILD, etc.
Cooldowns
rust
#[poise::command(
slash_command,
guild_only,
cooldown = 10, // 10 seconds cooldown
)]
pub async fn cool_command(ctx: Context<'_>) -> Result<(), Error> {
// Command logic
Ok(())
}
Category
rust
#[poise::command(
slash_command,
guild_only,
category = "Moderation" // Command category
)]
pub async fn mod_command(ctx: Context<'_>) -> Result<(), Error> {
// Command logic
Ok(())
}
Hide in Help
rust
#[poise::command(
slash_command,
guild_only,
hide_in_help // Hide from built-in help command
)]
pub async fn secret_command(ctx: Context<'_>) -> Result<(), Error> {
// Command logic
Ok(())
}
Command Parameters
Basic Types
rust
pub async fn parameters(
ctx: Context<'_>,
#[description = "A string value"] text: String,
#[description = "An integer value"] number: i64,
#[description = "A boolean value"] flag: bool,
#[description = "A Discord user"] user: serenity::User,
#[description = "A Discord member"] member: serenity::Member,
#[description = "A Discord role"] role: serenity::Role,
#[description = "A Discord channel"] channel: serenity::GuildChannel,
) -> Result<(), Error> {
// Command logic
Ok(())
}
Optional Parameters
rust
pub async fn optional_params(
ctx: Context<'_>,
#[description = "Optional text"] text: Option<String>,
#[description = "Optional user"] user: Option<serenity::User>,
) -> Result<(), Error> {
let text = text.unwrap_or_else(|| "default".to_string());
// Command logic
Ok(())
}
Rest Parameters (Multiple Values)
rust
pub async fn rest_params(
ctx: Context<'_>,
#[description = "Multiple values"]
#[rest] // Captures all remaining arguments
values: String,
) -> Result<(), Error> {
// Command logic
Ok(())
}
Choice Enumerations
rust
#[derive(poise::ChoiceParameter)]
enum Action {
#[name = "Ban"]
Ban,
#[name = "Kick"]
Kick,
#[name = "Mute"]
Mute,
}
pub async fn enum_param(
ctx: Context<'_>,
#[description = "Select an action"]
action: Action,
) -> Result<(), Error> {
match action {
Action::Ban => { /* ban logic */ }
Action::Kick => { /* kick logic */ }
Action::Mute => { /* mute logic */ }
}
Ok(())
}
Subcommands
Command Group with Subcommands
rust
#[poise::command(
slash_command,
subcommands("add", "remove", "list"),
category = "MyModule"
)]
pub async fn manage(
ctx: Context<'_>
) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Use a subcommand: /manage add, /manage remove, /manage list")
).await?;
Ok(())
}
/// Add something
#[poise::command(slash_command)]
pub async fn add(
ctx: Context<'_>,
#[description = "Item to add"] item: String,
) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content(format!("Added: {}", item))
).await?;
Ok(())
}
/// Remove something
#[poise::command(slash_command)]
pub async fn remove(
ctx: Context<'_>,
#[description = "Item to remove"] item: String,
) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content(format!("Removed: {}", item))
).await?;
Ok(())
}
/// List all items
#[poise::command(slash_command)]
pub async fn list(
ctx: Context<'_>,
) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("List of items...")
).await?;
Ok(())
}
Working with Context
Accessing Data
rust
pub async fn access_data(ctx: Context<'_>) -> Result<(), Error> {
// Access database
let db = &ctx.data().db;
// Access localization manager
let l10n = &ctx.data().l10n;
// Access logger service
let logger = &ctx.data().logger;
// Access punishment service
let punishment = &ctx.data().punishment;
// Access other services
let whitelist = &ctx.data().whitelist;
let cache = &ctx.data().cache;
let jail = &ctx.data().jail;
let temp_ban = &ctx.data().temp_ban;
Ok(())
}
Accessing Guild and User Info
rust
pub async fn guild_info(ctx: Context<'_>) -> Result<(), Error> {
// Get guild ID
let guild_id = ctx.guild_id().unwrap();
// Get author
let author = ctx.author();
// Get member
let member = ctx.author_member().await.unwrap().unwrap();
// Get locale
let locale = ctx.interaction().locale;
Ok(())
}
Localization Helpers
rust
use crate::services::localization::ContextL10nExt;
pub async fn localized(ctx: Context<'_>) -> Result<(), Error> {
// Get localized strings for the user
let l10n_user = ctx.l10n_user();
// Get localized strings for the guild
let l10n_guild = ctx.l10n_guild();
// Use with FluentArgs
use fluent::FluentArgs;
let mut args = FluentArgs::new();
args.set("user", "username");
args.set("count", 42);
let message = l10n_user.t("my-message-key", Some(&args));
ctx.send(poise::CreateReply::default()
.content(message)
).await?;
Ok(())
}
Response Types
Ephemeral Response (Only User Sees)
rust
#[poise::command(slash_command, ephemeral)]
pub async fn ephemeral_response(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Only you can see this!")
).await?;
Ok(())
}
Public Response
rust
#[poise::command(slash_command)]
pub async fn public_response(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Everyone can see this!")
).await?;
Ok(())
}
Edit Response
rust
#[poise::command(slash_command)]
pub async fn edit_response(ctx: Context<'_>) -> Result<(), Error> {
let reply = ctx.send(poise::CreateReply::default()
.content("Initial message")
).await?;
// Wait and edit
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
reply.edit(ctx, poise::CreateReply::default()
.content("Updated message!")
).await?;
Ok(())
}
Defer Long Operations
rust
#[poise::command(slash_command)]
pub async fn long_operation(ctx: Context<'_>) -> Result<(), Error> {
// Defer immediately to show "thinking" state
ctx.defer_ephemeral().await?;
// Perform long operation
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
// Send final response
ctx.send(poise::CreateReply::default()
.content("Operation completed!")
).await?;
Ok(())
}
Components in Responses
Buttons (Do Not Await!)
rust
pub async fn buttons(ctx: Context<'_>) -> Result<(), Error> {
let setup_id = generate_unique_id();
ctx.send(poise::CreateReply::default()
.content("Click a button!")
.components(vec![
serenity::CreateActionRow::Buttons(vec![
// Embed data in custom_id for later handling
serenity::CreateButton::new(format!("button_1_{}", setup_id))
.label("Button 1")
.style(serenity::ButtonStyle::Primary),
serenity::CreateButton::new(format!("button_2_{}", setup_id))
.label("Button 2")
.style(serenity::ButtonStyle::Danger),
])
])
).await?;
// ✅ Command returns immediately - DO NOT await button clicks here
Ok(())
}
// Button clicks are handled in separate event handler (see serenity-interactions skill)
Select Menus
rust
pub async fn selects(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(poise::CreateReply::default()
.content("Select an option!")
.components(vec![
serenity::CreateActionRow::SelectMenu(
serenity::CreateSelectMenu::new(
"select_menu_id",
serenity::CreateSelectMenuKind::String {
options: vec![
serenity::CreateSelectMenuOption::new("Option 1", "opt1"),
serenity::CreateSelectMenuOption::new("Option 2", "opt2"),
serenity::CreateSelectMenuOption::new("Option 3", "opt3"),
]
}
)
.placeholder("Choose an option")
)
])
).await?;
Ok(())
}
Error Handling
Returning Errors
rust
use anyhow::anyhow;
pub async fn error_handling(ctx: Context<'_>) -> Result<(), Error> {
let value = Some(42);
match value {
Some(v) => {
ctx.send(poise::CreateReply::default()
.content(format!("Value: {}", v))
).await?;
}
None => {
return Err(anyhow!("Value is missing!"));
}
}
Ok(())
}
User-Friendly Error Messages
rust
pub async fn user_errors(ctx: Context<'_>) -> Result<(), Error> {
if let Some(guild_id) = ctx.guild_id() {
// Do something
Ok(())
} else {
ctx.send(poise::CreateReply::default()
.content("This command can only be used in a server!")
).await?;
Ok(()) // Return Ok instead of error for better UX
}
}
Logging Actions
Using Logger Service
rust
use crate::services::logger::LogLevel;
use crate::db::entities::module_configs::ModuleType;
use fluent::FluentArgs;
pub async fn log_action(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let l10n_guild = ctx.l10n_guild();
// Log the action
let mut log_args = FluentArgs::new();
log_args.set("modId", ctx.author().id.get().to_string());
log_args.set("action", "ban");
ctx.data().logger.log_context(
&ctx,
Some(ModuleType::ModerationProtection),
LogLevel::Audit,
&l10n_guild.t("log-command-title", None),
&l10n_guild.t("log-command-desc", Some(&log_args)),
vec![
(&l10n_guild.t("log-field-moderator", None),
format!("<@{}>", ctx.author().id)),
(&l10n_guild.t("log-field-action", None), "Ban".to_string()),
],
).await?;
Ok(())
}
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
- •Use
defer_ephemeral()for long-running commands - •Check permissions before performing actions
- •Use localization for all user-facing text
- •Log important actions using the logger service
- •Handle errors gracefully with user-friendly messages
- •Use
ephemeralfor sensitive or private information - •Validate inputs before processing
- •Use descriptive parameter names for better Discord UI
Examples from Codebase
- •
src/modules/moderation_protection/commands/ban.rs- Command with optional parameters - •
src/services/help.rs- Help command implementation - •
src/services/config/mod.rs- Configuration commands with subcommands - •
src/services/status.rs- Simple status command - •
src/services/setup/mod.rs- Multi-step command with components