| name | poise-commands |
| description | Guide for creating slash commands using the Poise framework in the Open Guard bot Use when this capability is needed. |
| metadata | {"author":"erdemgksl"} |
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_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_click")
.label("Click Me!")
.style(serenity::ButtonStyle::Primary),
])
).await?;
let msg = ctx.channel_id().await_message(&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("Click the button!")
.components(vec![
serenity::CreateButton::new(format!("btn_click_{}", setup_id))
.label("Click Me!")
.style(serenity::ButtonStyle::Primary),
])
).await?;
Ok(())
}
Basic Command Structure
Simple Command
use crate::{Context, Error};
use poise::serenity_prelude as serenity;
#[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
#[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
#[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> {
Ok(())
}
Available permissions: ADMINISTRATOR, BAN_MEMBERS, KICK_MEMBERS, MANAGE_MESSAGES, MANAGE_GUILD, etc.
Cooldowns
#[poise::command(
slash_command,
guild_only,
cooldown = 10, // 10 seconds cooldown
)]
pub async fn cool_command(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
Category
#[poise::command(
slash_command,
guild_only,
category = "Moderation" // Command category
)]
pub async fn mod_command(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
Hide in Help
#[poise::command(
slash_command,
guild_only,
hide_in_help // Hide from built-in help command
)]
pub async fn secret_command(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
Command Parameters
Basic Types
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> {
Ok(())
}
Optional Parameters
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());
Ok(())
}
Rest Parameters (Multiple Values)
pub async fn rest_params(
ctx: Context<'_>,
#[description = "Multiple values"]
#[rest]
values: String,
) -> Result<(), Error> {
Ok(())
}
Choice Enumerations
#[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 => { }
Action::Kick => { }
Action::Mute => { }
}
Ok(())
}
Subcommands
Command Group with Subcommands
#[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(())
}
#[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(())
}
#[poise::command(slash_command)]
pub async fn remove(
ctx: Context<'_>,
#[description = "Item to remove"] item: String,
) -> <(), Error> {
ctx.(poise::CreateReply::()
.((, item))
).?;
(())
}
(
ctx: Context<>,
) <(), Error> {
ctx.(poise::CreateReply::()
.()
).?;
(())
}
Working with Context
Accessing Data
pub async fn access_data(ctx: Context<'_>) -> Result<(), Error> {
let db = &ctx.data().db;
let l10n = &ctx.data().l10n;
let logger = &ctx.data().logger;
let punishment = &ctx.data().punishment;
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
pub async fn guild_info(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let author = ctx.author();
let member = ctx.author_member().await.unwrap().unwrap();
let locale = ctx.interaction().locale;
Ok(())
}
Localization Helpers
use crate::services::localization::ContextL10nExt;
pub async fn localized(ctx: Context<'_>) -> Result<(), Error> {
let l10n_user = ctx.l10n_user();
let l10n_guild = ctx.l10n_guild();
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)
#[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
#[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
#[poise::command(slash_command)]
pub async fn edit_response(ctx: Context<'_>) -> Result<(), Error> {
let reply = ctx.send(poise::CreateReply::default()
.content("Initial message")
).await?;
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
reply.edit(ctx, poise::CreateReply::default()
.content("Updated message!")
).await?;
Ok(())
}
Defer Long Operations
#[poise::command(slash_command)]
pub async fn long_operation(ctx: Context<'_>) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
ctx.send(poise::CreateReply::default()
.content("Operation completed!")
).await?;
Ok(())
}
Components in Responses
Buttons (Do Not Await!)
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![
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?;
Ok(())
}
Select Menus
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
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
pub async fn user_errors(ctx: Context<'_>) -> Result<(), Error> {
if let Some(guild_id) = ctx.guild_id() {
Ok(())
} else {
ctx.send(poise::CreateReply::default()
.content("This command can only be used in a server!")
).await?;
Ok(())
}
}
Logging Actions
Using Logger Service
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();
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.().id)),
(&l10n_guild.(, ), .()),
],
).?;
(())
}
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
ephemeral for 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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.