> ## Documentation Index
> Fetch the complete documentation index at: https://anniemei.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding Commands

> Step-by-step guide to creating new Discord slash commands

This guide walks through creating a new slash command in Annie Mei, following the project's conventions and patterns.

## Command Structure

All commands follow a consistent pattern with two main functions:

* `register()` - Defines the slash command structure for Discord
* `run()` - Executes the command logic when invoked

Commands can be simple single-file modules or complex multi-file modules with subcommands.

## Creating a Simple Command

Let's create a `/ping` style command as an example.

<Steps>
  <Step title="Create the Command Module">
    Create a new file in `src/commands/`:

    ```bash theme={}
    touch src/commands/example.rs
    ```

    For complex commands, create a directory:

    ```bash theme={}
    mkdir -p src/commands/example
    touch src/commands/example/{mod.rs,command.rs}
    ```
  </Step>

  <Step title="Implement register() Function">
    Define the slash command structure that Discord will display to users:

    ```rust src/commands/example.rs theme={}
    use serenity::builder::CreateCommand;

    pub fn register() -> CreateCommand {
        CreateCommand::new("example")
            .description("An example command")
    }
    ```

    This creates a basic command with no options.
  </Step>

  <Step title="Implement run() Function">
    Add the command execution logic:

    ```rust src/commands/example.rs theme={}
    use serenity::{
        all::{CommandInteraction, CreateInteractionResponse, CreateInteractionResponseMessage},
        prelude::*,
    };
    use tracing::info;

    pub async fn run(ctx: &Context, interaction: &CommandInteraction) {
        info!("Received example command");

        let message = CreateInteractionResponseMessage::new()
            .content("Hello from example command!");
        let response = CreateInteractionResponse::Message(message);

        let _ = interaction.create_response(&ctx.http, response).await;
    }
    ```
  </Step>

  <Step title="Export in mod.rs">
    Add your new command to `src/commands/mod.rs`:

    ```rust src/commands/mod.rs theme={}
    pub mod anime;
    pub mod character;
    pub mod help;
    pub mod input_validation;
    pub mod manga;
    pub mod ping;
    pub mod recommend;
    pub mod register;
    pub mod response;
    pub mod search;
    pub mod songs;
    pub mod traits;
    pub mod unregister;
    pub mod whoami;
    pub mod example;  // Add this line
    ```
  </Step>

  <Step title="Register in main.rs ready Event">
    Add your command to the registration list in `src/main.rs` at the `ready` event handler:

    ```rust src/main.rs theme={}
    async fn ready(&self, ctx: Context, ready: Ready) {
        let commands: Vec<CreateCommand> = vec![
            commands::ping::register(),
            commands::help::register(),
            commands::songs::command::register(),
            commands::manga::command::register(),
            commands::anime::command::register(),
            commands::search::command::register(),
            commands::recommend::command::register(),
            commands::character::command::register(),
            commands::register::command::register(),
            commands::unregister::register(),
            commands::whoami::register(),
            commands::example::register(),  // Add this line
        ];

        let guild_commands = Command::set_global_commands(&ctx.http, commands).await;
        // ...
    }
    ```

    See `src/main.rs` for the full implementation.
  </Step>

  <Step title="Add Match Arm in interaction_create">
    Route the command to your handler in `src/main.rs` at the `interaction_create` event:

    ```rust src/main.rs theme={}
    match command.data.name.as_str() {
        "ping" => commands::ping::run(&ctx, &command).await,
        "help" => commands::help::run(&ctx, &command).await,
        "songs" => commands::songs::command::run(&ctx, &mut command).await,
        "manga" => commands::manga::command::run(&ctx, &mut command).await,
        "anime" => commands::anime::command::run(&ctx, &mut command).await,
        "search" => commands::search::command::run(&ctx, &mut command).await,
        "recommend" => commands::recommend::command::run(&ctx, &mut command).await,
        "character" => commands::character::command::run(&ctx, &mut command).await,
        "register" => commands::register::command::run(&ctx, &mut command).await,
        "unregister" => commands::unregister::run(&ctx, &mut command).await,
        "whoami" => commands::whoami::run(&ctx, &mut command).await,
        "example" => commands::example::run(&ctx, &mut command).await,  // Add this line
        _ => {
            // Error handler...
        }
    };
    ```

    See `src/main.rs` for the full implementation.
  </Step>
</Steps>

## Adding Command Options

Commands can accept user input through options:

```rust theme={}
use serenity::{
    all::CreateCommandOption,
    builder::CreateCommand,
    model::application::CommandOptionType,
};

pub fn register() -> CreateCommand {
    CreateCommand::new("search")
        .description("Search for something")
        .add_option(
            CreateCommandOption::new(
                CommandOptionType::String,
                "query",
                "What to search for",
            )
            .required(true),
        )
        .add_option(
            CreateCommandOption::new(
                CommandOptionType::Integer,
                "limit",
                "Maximum number of results",
            )
            .required(false)
            .min_int_value(1)
            .max_int_value(25),
        )
}
```

### Accessing Option Values

Extract option values in your `run()` function:

```rust theme={}
pub async fn run(ctx: &Context, interaction: &mut CommandInteraction) {
    // Get required string option
    let Some(serenity::all::CommandDataOptionValue::String(query)) =
        interaction.data.options.first().map(|opt| &opt.value)
    else {
        let builder = EditInteractionResponse::new()
            .content("Missing required 'query' option");
        let _ = interaction.edit_response(&ctx.http, builder).await;
        return;
    };

    // Get optional integer option
    let limit = interaction.data.options
        .get(1)
        .and_then(|opt| match &opt.value {
            serenity::all::CommandDataOptionValue::Integer(val) => Some(*val),
            _ => None,
        })
        .unwrap_or(10);  // Default value

    info!("Search query: {}, limit: {}", query, limit);
    // Command logic...
}
```

See `src/commands/anime/command.rs:66-80` for a real-world example.

## Deferring Long Operations

Discord requires responses within 3 seconds. For long-running operations, defer the response immediately:

```rust theme={}
#[instrument(name = "command.example.run", skip(ctx, interaction))]
pub async fn run(ctx: &Context, interaction: &mut CommandInteraction) {
    // Defer immediately
    let _ = interaction.defer(&ctx.http).await;

    // Long operation (e.g., async API call or database query)
    let result = fetch_example_data().await;

    // Edit the deferred response
    let builder = EditInteractionResponse::new()
        .content("Operation complete!");
    let _ = interaction.edit_response(&ctx.http, builder).await;
}
```

<Warning>
  Always defer before operations that take longer than 3 seconds, including external API calls and database queries.
</Warning>

## Calling external services

External API clients use async `reqwest::Client`. Await those requests directly after deferring the Discord interaction:

```rust theme={}
use tracing::error;

let result = match client
    .post("https://graphql.anilist.co/")
    .header("Content-Type", "application/json")
    .body(query_json.to_string())
    .send()
    .await
{
    Ok(response) => Some(response),
    Err(error) => {
        error!(error = %error, "API request failed");
        None
    }
};
```

Use `spawn_blocking` only for synchronous work that can block the Tokio runtime, such as the current Redis cache helper calls.

See `src/utils/requests/anilist.rs` for the AniList HTTP client and `src/commands/recommend/command.rs` for a bounded `spawn_blocking` cache example.

## Adding Instrumentation

Use the `#[instrument]` attribute for tracing and observability:

```rust theme={}
use tracing::{info, instrument};

#[instrument(name = "command.example.run", skip(ctx, interaction))]
pub async fn run(ctx: &Context, interaction: &mut CommandInteraction) {
    info!("Processing example command");
    // Command logic...
}
```

This creates a tracing span that appears in logs and Sentry:

* `name` - Span name for filtering
* `skip` - Fields to exclude from tracing (avoid logging sensitive data)

## Code Style Conventions

Follow these conventions when writing commands:

<CardGroup cols={2}>
  <Card title="Logging" icon="terminal">
    Use `tracing` macros: `info!`, `debug!`, `error!`

    Never use `println!` or `eprintln!`
  </Card>

  <Card title="Error Handling" icon="circle-exclamation">
    Prefer `?` operator over `.unwrap()`

    Handle errors gracefully with user-friendly messages
  </Card>

  <Card title="Privacy" icon="shield">
    Hash user IDs before logging:

    ```rust theme={}
    use crate::utils::privacy::hash_user_id;

    let hashed = hash_user_id(user.id.get());
    ```
  </Card>

  <Card title="Sentry" icon="radar">
    Configure Sentry scope for commands:

    ```rust theme={}
    use crate::utils::privacy::configure_sentry_scope;

    configure_sentry_scope("Example", user.id.get(), None);
    ```
  </Card>
</CardGroup>

## Testing Your Command

### Unit Tests

Add tests in the same file:

```rust theme={}
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_command_logic() {
        // Test your command's core logic
        assert_eq!(2 + 2, 4);
    }
}
```

See `src/commands/ping.rs:50-78` for a complete test example.

### Running Tests

```bash theme={}
cargo test
```

Run tests for a specific module:

```bash theme={}
cargo test --test example
```

### Manual Testing

1. Run the bot locally:
   ```bash theme={}
   cargo run
   ```

2. Use the command in Discord:
   ```
   /example
   ```

3. Check logs for tracing output:
   ```bash theme={}
   RUST_LOG=debug cargo run
   ```

## Example: Complete Command

Here's a complete command implementation from `src/commands/ping.rs`:

```rust theme={}
use crate::utils::privacy::configure_sentry_scope;
use super::response::CommandResponse;

use serenity::{
    all::{CommandInteraction, CreateInteractionResponse, CreateInteractionResponseMessage},
    builder::CreateCommand,
    prelude::*,
};

pub fn register() -> CreateCommand {
    CreateCommand::new("ping").description("A ping command")
}

// Core logic (transport-agnostic, testable)
pub fn handle_ping(user_mention: &str) -> CommandResponse {
    CommandResponse::Message(format!(
        "Hello {user_mention}! I'm Annie Mei, a bot that helps you find anime and manga!",
    ))
}

// Serenity adapter (thin wrapper)
pub async fn run(ctx: &Context, interaction: &CommandInteraction) {
    let user = &interaction.user;
    configure_sentry_scope("Ping", user.id.get(), None);

    let reply = handle_ping(&user.mention().to_string());

    let text = match reply {
        CommandResponse::Message(text) => text,
        _ => unreachable!("/ping always returns Message"),
    };

    let response_message = CreateInteractionResponseMessage::new().content(text);
    let response = CreateInteractionResponse::Message(response_message);

    let _ = interaction.create_response(&ctx.http, response).await;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ping_happy_path_returns_message_with_greeting() {
        let response = handle_ping("<@123456>");
        assert!(response.is_message());
        let text = response.unwrap_message();
        assert!(text.contains("<@123456>"));
        assert!(text.contains("Annie Mei"));
    }
}
```

## File Organization Patterns

Depending on complexity, organize your command:

### Simple Command (Single File)

```
src/commands/ping.rs
```

Use for commands with minimal logic and no external queries.

### Complex Command (Module)

```
src/commands/anime/
├── mod.rs          # Re-exports
├── command.rs      # register() and run()
└── queries.rs      # GraphQL queries
```

Use for commands with:

* GraphQL queries
* Multiple helper functions
* Complex business logic

See `src/commands/anime/` for a complete example.

## Next Steps

<CardGroup cols={2}>
  <Card title="Architecture" icon="sitemap" href="/development/architecture">
    Understand the project structure
  </Card>

  <Card title="Database" icon="database" href="/development/database">
    Add database models for persistent data
  </Card>

  <Card title="Testing" icon="flask" href="/development/testing">
    Write comprehensive tests
  </Card>
</CardGroup>
