スキル一覧に戻る
ErdemGKSL

poise-commands

by ErdemGKSL

0🍴 0📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: poise-commands description: Guide for creating slash commands using the Poise framework in the Open Guard bot metadata: version: "1.0"

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

// ❌ 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

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

/// 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

#[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

#[poise::command(
    slash_command,
    guild_only,
    cooldown = 10,  // 10 seconds cooldown
)]
pub async fn cool_command(ctx: Context<'_>) -> Result<(), Error> {
    // Command logic
    Ok(())
}

Category

#[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

#[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

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

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)

pub async fn rest_params(
    ctx: Context<'_>,
    #[description = "Multiple values"]
    #[rest]  // Captures all remaining arguments
    values: String,
) -> Result<(), Error> {
    // Command logic
    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 => { /* ban logic */ }
        Action::Kick => { /* kick logic */ }
        Action::Mute => { /* mute logic */ }
    }
    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(())
}

/// 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

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

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

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)

#[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?;

    // 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

#[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!)

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

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() {
        // 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

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

  1. ⚠️ NEVER await interactions in commands: Commands must return immediately
  2. ⚠️ ALWAYS use separate handlers: Handle interactions in event handlers, not commands
  3. ⚠️ ALWAYS use custom_id parsing: Transport data via custom_ids, not awaiting
  4. Use defer_ephemeral() for long-running commands
  5. Check permissions before performing actions
  6. Use localization for all user-facing text
  7. Log important actions using the logger service
  8. Handle errors gracefully with user-friendly messages
  9. Use ephemeral for sensitive or private information
  10. Validate inputs before processing
  11. 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

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です