discordrs 0.3.1

Standalone Discord bot framework with Components V2, Gateway, and HTTP client
Documentation
# discordrs Usage

`discordrs` is a standalone Discord bot framework for Rust that includes Components V2 builders, a Gateway WebSocket client, and an HTTP client.

## 1. Installation

A common setup for running a Gateway bot runtime is:

```toml
[dependencies]
discordrs = { version = "0.3.1", features = ["gateway"] }
```

You can choose feature flags depending on your use case.

```toml
[dependencies]
# Core only (builders, parsers, HTTP client, helpers)
discordrs = "0.3.1"

# Gateway + bot client runtime
discordrs = { version = "0.3.1", features = ["gateway"] }

# Interactions Endpoint
discordrs = { version = "0.3.1", features = ["interactions"] }

# Both runtime modes
discordrs = { version = "0.3.1", features = ["gateway", "interactions"] }
```

## 2. Start a Bot

```rust
use async_trait::async_trait;
use discordrs::{gateway_intents, BotClient, Context, EventHandler};
use serde_json::Value;

struct Handler;

#[async_trait]
impl EventHandler for Handler {
    async fn ready(&self, _ctx: Context, ready: Value) {
        println!("READY: {}", ready["user"]["username"]);
    }

    async fn message_create(&self, _ctx: Context, message: Value) {
        println!("MESSAGE_CREATE: {}", message["id"]);
    }
}

#[tokio::main]
async fn main() -> Result<(), discordrs::Error> {
    let token = std::env::var("DISCORD_TOKEN")?;

    BotClient::builder(
        &token,
        gateway_intents::GUILDS | gateway_intents::GUILD_MESSAGES,
    )
    .event_handler(Handler)
    .start()
    .await?;

    Ok(())
}
```

## 3. Send a Container Message to a Channel

```rust
use discordrs::{button_style, create_container, send_container_message, ButtonConfig, DiscordHttpClient};

async fn send_panel(http: &DiscordHttpClient, channel_id: u64) -> Result<(), discordrs::Error> {
    let buttons = vec![
        ButtonConfig::new("ticket_open", "Open Ticket").style(button_style::PRIMARY),
        ButtonConfig::new("ticket_status", "Check Status").style(button_style::SECONDARY),
    ];

    let container = create_container(
        "Support Panel",
        "Use the buttons below to submit a request or check its status.",
        buttons,
        None,
    );

    send_container_message(http, channel_id, container).await?;
    Ok(())
}
```

## 4. Respond to a Slash Command

With `InteractionContext`, you can directly access `interaction_id`, `token`, and `application_id` for replies.

```rust
use discordrs::{
    create_container, parse_interaction_context, parse_raw_interaction,
    respond_with_container, DiscordHttpClient, RawInteraction,
};
use serde_json::Value;

async fn handle_slash(http: &DiscordHttpClient, payload: &Value) -> Result<(), discordrs::Error> {
    let ctx = parse_interaction_context(payload)?;

    if let RawInteraction::Command { name, .. } = parse_raw_interaction(payload)? {
        if name.as_deref() == Some("hello") {
            let container = create_container("Notice", "Your command has been processed.", vec![], None);
            respond_with_container(http, &ctx.id, &ctx.token, container, true).await?;
        }
    }

    Ok(())
}
```

## 5. Build an Interactions Endpoint Safely

Use `try_interactions_endpoint()` when you want invalid public keys to fail at startup instead of turning into runtime 500s.

```rust
use async_trait::async_trait;
use axum::Router;
use discordrs::{
    create_container, InteractionContext, InteractionHandler, InteractionResponse, RawInteraction,
    try_interactions_endpoint,
};

#[derive(Clone)]
struct Handler;

#[async_trait]
impl InteractionHandler for Handler {
    async fn handle(
        &self,
        _ctx: InteractionContext,
        interaction: RawInteraction,
    ) -> InteractionResponse {
        match interaction {
            RawInteraction::Command { name, .. } if name.as_deref() == Some("hello") => {
                InteractionResponse::ChannelMessage(serde_json::json!({
                    "components": [create_container("Hello", "From interactions endpoint", vec![], None).build()],
                    "flags": 1 << 15,
                }))
            }
            _ => InteractionResponse::DeferredMessage,
        }
    }
}

fn build_router(public_key: &str) -> Router {
    try_interactions_endpoint(public_key, Handler).expect("invalid Discord public key")
}
```

## 6. Send a Follow-up When You Already Have application_id

If the HTTP client was created without an application id, use the explicit follow-up method with `InteractionContext.application_id` instead of relying on an unset client value.

```rust
use discordrs::{parse_interaction_context, DiscordHttpClient};
use serde_json::Value;

async fn send_followup(http: &DiscordHttpClient, payload: &Value) -> Result<(), discordrs::Error> {
    let ctx = parse_interaction_context(payload)?;
    let body = serde_json::json!({
        "content": "Follow-up message",
    });

    http.create_followup_message_with_application_id(&ctx.application_id, &ctx.token, &body)
        .await?;

    Ok(())
}
```

## 7. Respond to Button/Select Interactions

```rust
use discordrs::{
    create_container, parse_interaction_context, respond_component_with_container,
    DiscordHttpClient,
};
use serde_json::Value;

async fn handle_component(http: &DiscordHttpClient, payload: &Value) -> Result<(), discordrs::Error> {
    let ctx = parse_interaction_context(payload)?;
    let container = create_container("Processed", "The selected value has been saved.", vec![], None);

    respond_component_with_container(http, &ctx.id, &ctx.token, container, true).await?;
    Ok(())
}
```

## 8. Respond to Modal Submissions

From `RawInteraction::ModalSubmit`, you can read Radio/Checkbox/FileUpload values from `V2ModalSubmission` without losing V2 structure.

```rust
use discordrs::{
    create_container, parse_interaction_context, parse_raw_interaction,
    respond_modal_with_container, DiscordHttpClient, RawInteraction, V2ModalSubmission,
};
use serde_json::Value;

fn summarize(submission: &V2ModalSubmission) -> String {
    let theme = submission.get_radio_value("theme").unwrap_or("Not selected");
    let channels = submission
        .get_select_values("notify_channels")
        .map(|v| v.join(", "))
        .unwrap_or_else(|| "None".to_string());
    let files = submission
        .get_file_values("attachments")
        .map(|v| v.join(", "))
        .unwrap_or_else(|| "No files".to_string());

    format!("Theme: {theme}, Notifications: {channels}, Files: {files}")
}

async fn handle_modal(http: &DiscordHttpClient, payload: &Value) -> Result<(), discordrs::Error> {
    let ctx = parse_interaction_context(payload)?;

    if let RawInteraction::ModalSubmit(submission) = parse_raw_interaction(payload)? {
        let result = summarize(&submission);
        let container = create_container("Modal Processed", &result, vec![], None);
        respond_modal_with_container(http, &ctx.id, &ctx.token, container, true).await?;
    }

    Ok(())
}
```

## 9. Modal Radio/Checkbox/FileUpload Example

```rust
use discordrs::{
    CheckboxBuilder, CheckboxGroupBuilder, FileUploadBuilder, ModalBuilder, RadioGroupBuilder,
    SelectOption,
};

let modal = ModalBuilder::new("preferences_modal", "Preferences")
    .add_radio_group(
        "Theme",
        Some("Pick one"),
        RadioGroupBuilder::new("theme")
            .add_option(SelectOption::new("Light", "light"))
            .add_option(SelectOption::new("Dark", "dark"))
            .required(true),
    )
    .add_checkbox_group(
        "Notification Channels",
        Some("You can select multiple options"),
        CheckboxGroupBuilder::new("notify_channels")
            .add_option(SelectOption::new("Email", "email"))
            .add_option(SelectOption::new("Push", "push"))
            .min_values(0)
            .max_values(2),
    )
    .add_checkbox(
        "Agree to Terms",
        None,
        CheckboxBuilder::new("agree_terms").required(true),
    )
    .add_file_upload(
        "Screenshot",
        Some("Attach one or more files"),
        FileUploadBuilder::new("attachments").min_values(1),
    );
```

## 10. Frequently Used APIs

- `DiscordHttpClient::new(token, application_id)`: Create a REST client
- `create_container(...)`: Build a base Components V2 container message
- `send_container_message(...)`: Send a Components V2 message to a channel
- `respond_with_container(...)`: Reply to a Slash Command
- `try_interactions_endpoint(...)`: Build an interactions endpoint with startup-time public key validation
- `DiscordHttpClient::create_followup_message_with_application_id(...)`: Send follow-up replies when `InteractionContext.application_id` is available
- `respond_component_with_container(...)`: Reply to button/select interactions
- `respond_modal_with_container(...)`: Reply to modal submissions
- `respond_with_modal(...)`: Open a modal as a response
- `parse_raw_interaction(...)`: Route by interaction type
- `parse_interaction_context(...)`: Extract shared context required for replies
- `parse_modal_submission(...)`: Parse V2 modal submission data

## 11. Notes

- Since v0.3.1, `discordrs` is a standalone framework that provides both Gateway and HTTP capabilities.
- The V2 modal parser preserves component types such as `FileUpload`, `Label`, `RadioGroup`, `CheckboxGroup`, and `Checkbox`, which helps downstream processing.
- Interaction response helpers can directly use `id` and `token` from `InteractionContext`.
- Follow-up webhook helpers now fail early if `application_id` is missing instead of emitting `/webhooks/0/...`.