Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REST and deferring

Handlers can call Discord’s REST API, and answer later than Discord’s 3 seconds allow by deferring: replying “thinking…” at once and finishing in the background. Both need a State that implements Platform, which every Adapter’s State does; take ctx: Ctx<impl Platform> to say so and keep the handler testable off any platform.

Enable the rest feature (the rest client and Interaction Endpoints), plus the rest-* features for the BotClient Endpoints you call, or tier1 for the common ones. Beyond tier1 are groups such as Linked Roles metadata (rest-role-connections), onboarding and the welcome screen (rest-onboarding), stage instances (rest-stage-instances) and lobbies (rest-lobbies); full turns on every one. The feature flags list them all.

Calling the API

use composure::{command, command::{Ctx, HandlerError, Platform}};

/// Ask the bot who it is
#[command]
async fn whoami(ctx: Ctx<impl Platform>) -> Result<String, HandlerError> {
    let me = ctx.bot()?.get_current_user().await?.model()?;
    Ok(format!("I'm {}.", me.display_name()))
}
fn main() {}
ClientAuthorized byFor
ctx.client() → InteractionClientthe Interaction’s token (valid 15 minutes)follow-up messages, the original response: followup, create_followup, edit_original, delete_original, …
ctx.bot() → BotClientthe bot token (DISCORD_TOKEN); an error when the Adapter has noneeverything else: messages, members, roles, channels, Commands, … (one rest-* feature per group)

Every call returns a Response<T> whose body is decoded only when you ask with .model(), so calls whose result you ignore skip the parse. Errors are rest::Errors, which convert into HandlerError with ?. Rate limits (429) are waited out, up to with_max_rate_limit_wait (2 seconds by default), then returned as Error::RateLimited.

Inline calls count toward the 3 seconds. One or two fast calls are fine; anything slower should defer.

Deferring

ctx.defer(work) answers with a Deferred Response (the user sees “bot is thinking…”) and runs work as Background Work after the response is sent. When it finishes, its output replaces the “thinking” message, exactly as if the handler had returned it: text, an embed, a View, a Result (an Err becomes the error message), files included.

use std::time::Duration;
use composure::{command, command::{Ctx, Deferred, Ephemeral, HandlerError, Platform}};

/// Count the members of this server (slowly)
#[command]
fn census(ctx: Ctx<impl Platform>) -> Ephemeral<Deferred> {
    Ephemeral(ctx.defer(async |ctx| {
        ctx.sleep(Duration::from_secs(5)).await; // pretend this is slow
        let guild = ctx.guild_id().ok_or_else(|| HandlerError::user("Use this in a server."))?;
        Ok::<_, HandlerError>(format!("Counted the members of {guild}."))
    }))
}
fn main() {}
  • The closure receives a DeferredCtx: an owned, 'static copy of the Interaction and State, with the same user(), guild_id(), client(), bot(), plus sleep on the platform’s timer.
  • Its future must be Send + 'static, so it owns what it uses: convert borrowed parameters first (let text = text.to_owned();) and move them in.
  • Wrap the result of defer in Ephemeral to defer ephemerally; the final message is then ephemeral too, whatever work returns.
  • A failing work edits the response with the error, rendered as the App renders errors (errors = ..), and is logged.
  • Background Work has a time limit set by the platform: about 30 seconds after the response on Cloudflare Workers (wait_until), until shutdown’s grace period on tokio. The Interaction token, which the final edit uses, expires after 15 minutes.

Updating a component’s message later

For components (and modals opened from one), ctx.defer_update(work) acknowledges without any visible loading state and then replaces the message the component is on with work’s output. If work fails, the message is left as it was (buttons and state intact) and the error is shown in an ephemeral follow-up instead:

use std::time::Duration;
use composure::{component, command::{ComponentCtx, DeferredUpdate, HandlerError, Platform}};

/// Re-roll a die after a dramatic pause
#[component("roll")]
fn reroll(ctx: ComponentCtx<impl Platform>, rolls: u32) -> DeferredUpdate {
    ctx.defer_update(async move |ctx| {
        ctx.sleep(Duration::from_secs(1)).await;
        let face = ctx.interaction().common.id.to_u64() % 6 + 1;
        Ok::<_, HandlerError>(format!("Rolled a {face} ({} rolls)", rolls + 1))
    })
}
fn main() {}

Inline View handlers can do the same, returning new Props from work to re-render the View.

Follow-ups and progress

Background Work can talk more than once: edit the original response as it goes, and send follow-up messages (new messages under the original).

use std::time::Duration;
use composure::{command, command::{Ctx, Deferred, HandlerError, Platform}, rest::EditMessage};

/// Count down, editing the response every second
#[command]
fn countdown(ctx: Ctx<impl Platform>) -> Deferred {
    ctx.defer(async |ctx| {
        let client = ctx.client();
        for n in (1..=3).rev() {
            let content = format!("{n}...");
            client.edit_original(&EditMessage { content: Some(&content), ..Default::default() }).await?;
            ctx.sleep(Duration::from_secs(1)).await;
        }
        client.followup("Liftoff! 🚀").await?;
        Ok::<_, HandlerError>("Done.")
    })
}
fn main() {}

Request bodies (EditMessage, CreateFollowup, …) borrow their fields; absent fields are left out of the request. MessageCallbackData converts into both. Build them with the same Views and colors as your replies to match (Styling).

Outside Interactions

A BotClient can also be built from any Transport, a bot token and the application id, for cron jobs, queues or scripts: BotClient::new(transport, &token, application_id), or Credentials::bot_client(transport). The Adapters provide one (bot_client(&env) on Workers, host.bot() on tokio), and the reqwest feature adds a blocking ReqwestTransport for deploy-time tools. See Platforms.

Webhook Event handlers get the bot client from their EventCtx; see Webhook Events.