Getting started
From an empty crate to a bot answering /hello: define Commands, collect them in an App, serve
the App with an Adapter, and sync its Commands with Discord.
How a bot works
Discord sends every Interaction (a slash Command, a button click, a modal submit, an
autocomplete request) as a signed HTTP POST to the bot’s Interactions Endpoint URL. The
bot verifies the signature, answers within 3 seconds, and can keep working afterwards through
the REST API. There is no gateway connection to keep alive, so the bot can run where requests
are cheap and start fast: a serverless platform like Cloudflare Workers, or any server.
Composure splits this in three:
- Your bot, written against
composure: Commands and handlers collected into an#[app]. Platform-agnostic. - An Adapter, which serves the App on one platform: it reads the request, validates the
signature, dispatches to the App, and writes the response.
composure_adapter_cloudflareandcomposure_adapter_tokioexist today. - Sync (
composure_sync), run at deploy time, which registers the App’s Command Definitions with Discord.
Start from a template
To skip the setup, generate a bot with cargo-generate,
then follow Local development to run it:
cargo generate --git https://github.com/BlueFrog130/composure-rs templates/cloudflare --name my-bot
cargo generate --git https://github.com/BlueFrog130/composure-rs templates/tokio --name my-bot
The rest of this chapter builds the same bot by hand.
Setting up
Create an application in the Developer Portal and note its application id, public key and (under Bot) token.
Add the crates. On Cloudflare Workers (a cdylib built with worker-build):
[dependencies]
composure = { version = "0.2", features = ["macros", "tier1"] }
composure_adapter_cloudflare = "0.2"
worker = "0.8"
On a server:
[dependencies]
composure = { version = "0.2", features = ["macros", "tier1"] }
composure_adapter_tokio = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
macros brings the attribute macros; tier1 the REST client with the Endpoints most bots need.
See feature flags for the rest; leaving REST out keeps a Worker’s bundle
smaller. For tests, testing adds stand-ins for Discord and the platform (Testing).
A first Command
use composure::{app, command, command::Ctx};
/// Say hello
#[command]
fn hello(ctx: Ctx) -> String {
let name = ctx.user().map_or("stranger", |user| user.display_name());
format!("Hello, {name}!")
}
/// Everything the bot serves
#[app(handlers(hello))]
pub struct Bot;
fn main() {}
The function is the handler; the doc comment, the name and the parameters are the definition Discord shows. Commands covers Options, Groups and more.
Apps
#[app(handlers(..))] on a unit struct makes an App: the handler an Adapter
serves and the set of Commands Sync registers. It lists, in any order and mix:
- Commands (
#[command]) and Groups (#[group]) - component and modal handlers (
#[component],#[modal]) - Views with inline handlers (
#[view]) - other Apps
Apps nest, so each module of a bot can declare its own App next to its handlers, and the root App lists the modules’:
mod moderation {
use composure::{app, command, types::User};
/// Kick a member
#[command]
fn kick(#[option("Who")] user: &User) -> String {
format!("Kicked {}", user.display_name())
}
#[app(handlers(kick))]
pub struct Moderation;
}
mod fun {
use composure::{app, command};
/// Flip a coin
#[command]
fn flip() -> &'static str {
"Heads"
}
#[app(handlers(flip))]
pub struct Fun;
}
use composure::app;
#[app(handlers(moderation::Moderation, fun::Fun))]
pub struct Bot;
fn main() {}
#[app] also takes events = .. (Webhook Events) and errors = .., how handler
errors are answered (Styling).
Mistakes that would only show up at runtime fail to compile instead: two Commands with the same
name and type, two handlers with the same custom_id prefix, more Commands in a
Scope than Discord allows, a sub-View with handlers that no App
lists, two Apps setting events (or errors).
An App implements InteractionHandler (what Adapters
serve), EventHandler, and
CommandSetDefinition (what Sync registers).
Interactions nothing in the App handles get a default answer, or go to a fallback set with
Bot.with_fallback(handler) (Fallback).
Serving it
Cloudflare Workers
use composure_adapter_cloudflare::CloudflareInteractionBot;
use worker::{Context, Env, Request, Response, event};
#[event(fetch)]
async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
CloudflareInteractionBot::new(req, env, ctx)
.with_handler(Bot)
.process()
.await
}
Set the secrets with wrangler secret put DISCORD_PUBLIC_KEY (required) and DISCORD_TOKEN
(for the bot client), or load the Credentials another
way with .with_credentials(..); deploy, and put https://<worker>.workers.dev/ in the Developer Portal as
the Interactions Endpoint URL.
A server, with tokio
use composure_adapter_tokio::TokioInteractionBot;
#[tokio::main]
async fn main() -> Result<(), composure_adapter_tokio::Error> {
TokioInteractionBot::from_env()? // DISCORD_PUBLIC_KEY, DISCORD_TOKEN, DISCORD_APPLICATION_ID, PORT
.with_handler(Bot)
.serve() // 0.0.0.0:$PORT (8080), until Ctrl-C / SIGTERM
.await
}
from_env is one way to get the Credentials:
TokioInteractionBot::new(credentials) takes them from anywhere, the config crate included.
It speaks plain HTTP: put it behind a TLS-terminating proxy or tunnel, and use that HTTPS URL as
the Interactions Endpoint URL.
Discord checks the URL when you save it (a signed Ping, which the Adapter answers), so the bot must be running first. See Platforms for State, Bot Data (your own configuration), platform-specific Commands and logging.
Syncing Commands
Discord only shows Commands it has stored. composure_sync
makes the stored ones match the App: from a host-only binary in your crate,
// src/bin/sync.rs
fn main() -> std::process::ExitCode {
composure_sync::sync_from_env(&my_bot::Bot)
}
DISCORD_TOKEN=.. DISCORD_APPLICATION_ID=.. cargo run --bin sync
It prints a plan (Commands to create, update and delete, per target), and applies it after
confirmation; --dry-run, --check (CI drift check), --yes (CI deploys) and --guild <id>
(send everything to one test server) adjust that.
Commands with a Scope go to the guilds listed for it:
COMPOSURE_SCOPE_DEV=123,456 for scope = "dev". Sync again whenever a Command’s definition
changes; handlers can change freely without it.
On Workers, keep Sync out of the Worker build: put it in a [[bin]] that only builds on the
host, as the repository’s examples/cloudflare does.
Next steps
- Commands: Options, Choices, autocomplete, Groups, context menus
- Responses: embeds, ephemeral messages, files, errors
- Views: rich Components V2 messages and modals in markup
- REST and deferring: calling Discord, answering slowly
- The repository’s
examples/hold a complete bot served both ways.