composure 0.2.4

Discord bots over HTTP Interactions, no gateway: slash commands, components, modals and JSX-like views, on Cloudflare Workers (WASM) or any server
Documentation

Composure

Discord bots that live on the edge.

Slash commands, buttons, modals and rich Components V2 layouts, written once in Rust and served from Cloudflare Workers or any server. No gateway, no idle process, no bill while nobody is talking to your bot.

Crates.io Docs.rs CI Downloads License MSRV Discord Discord API Cloudflare Workers tokio WASM unsafe forbidden

Guide · API docs · Examples · Discord · Crates


use composure::{app, command, types::User};

/// Give someone a cookie
#[command]
fn cookie(
    #[option("Who gets it")] user: &User,
    #[option("How many", min = 1, max = 12)] count: Option<u8>,
) -> String {
    format!("{} gets {} 🍪", user.display_name(), count.unwrap_or(1))
}

#[app(handlers(cookie))]
pub struct Bot;

The function is the command. Its parameters become Discord Options (with min/max enforced by Discord and checked against u8 at compile time), its doc comment the description, its return value the reply. Hand Bot to an Adapter to serve it, and to composure_sync to register it.

Why Composure?

Composure handles Interactions only: commands, buttons, selects and modals that Discord delivers to your bot as signed HTTP requests.

  • Serverless. No WebSocket to keep alive, no process to babysit. Deploy to Cloudflare Workers and pay per request, or run it on any box with tokio.
  • Written once. One function per command: the definition Discord stores and the handler that answers come from the same code, so they can never drift apart.
  • Typed end to end. Options arrive as &str, u8, &User, Role or your own enums. Duplicate commands, too many Options, out-of-range bounds and bad custom_id prefixes fail to compile, not in production.
  • Markup for messages. Build Components V2 messages and modals with JSX-like rsx!, with click handlers right next to the buttons they belong to.
  • State in the custom_id. Button and modal state is encoded into the custom_id for you, so a counter needs no database.
  • Small and fast. A few microseconds and a few dozen allocations per Interaction; the full example bot is about 307 KB of gzipped WASM. The core has no async runtime and no HTTP stack.
  • REST and more. A REST client for every Discord endpoint, deferred responses with background work, follow-ups, file uploads, Webhook Events, tracing, and a command sync CLI with CI drift checks.
Gateway bot Composure
Connection A WebSocket, always open None: Discord calls you
Hosting A server running 24/7 Cloudflare Workers, a container, any server
Scales to zero No Yes
Commands defined Often twice (registration + handler) Once, as a function
Reads every message Possible (with intents) No: Interactions and Webhook Events only

If your bot needs to watch every message or presence update, use a gateway library. If it only answers people, Composure needs no gateway connection, and on Workers no server at all.

A tour

Rich messages with rsx! and #[view]

use composure::{rsx, view, command::View};

/// A card that counts its likes. The count lives in the button's custom_id.
#[view]
pub fn Card(title: String, likes: u32) -> View {
    rsx! {
        <Container>
            "## " {title.as_str()}
            <Text>"-# " {likes} " likes"</Text>
            <Row>
                <Button primary on:click:like=move |_| Card { likes: likes + 1, title }>
                    "Like"
                </Button>
                <Button url="https://discord.com/developers/docs">"Docs"</Button>
            </Row>
        </Container>
    }
}

Clicking Like re-renders the message with new Props. Every Discord component has a tag, and every tag's attributes are documented.

Typed modal forms

use composure::{Choice, Modal, modal, command::Ephemeral, types::Attachment};

#[derive(Choice, Debug)]
pub enum Kind { Bug, Idea }

#[derive(Modal)]
#[modal(title = "Feedback")]
pub struct Feedback {
    #[input(max_len = 100)]
    topic: String,
    #[radio]
    kind: Kind,
    #[file_upload(max = 3, file_types("image"))]
    screenshots: Vec<Attachment>,
}

#[modal("feedback")]
fn on_feedback(form: Feedback) -> Ephemeral<String> {
    Ephemeral(format!("Thanks! {:?}: {} ({} screenshots)", form.kind, form.topic, form.screenshots.len()))
}

Slow work with defer

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

/// Crunch some numbers
#[command]
fn crunch(ctx: Ctx<impl Platform>) -> Deferred {
    ctx.defer(async |ctx| {
        ctx.sleep(Duration::from_secs(5)).await; // well past Discord's 3 seconds
        let me = ctx.bot()?.get_current_user().await?.model()?;
        Ok::<_, HandlerError>(format!("{} finished crunching", me.username))
    })
}

Serve it

// Cloudflare Workers
#[event(fetch)]
async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
    CloudflareInteractionBot::new(req, env, ctx).with_handler(Bot).process().await
}

// Any server
#[tokio::main]
async fn main() -> Result<(), composure_adapter_tokio::Error> {
    TokioInteractionBot::from_env()?.with_handler(Bot).serve().await
}

Sync it

cargo run --bin sync             # show what changes on Discord, apply after confirming
cargo run --bin sync -- --check  # in CI: fail when Discord is out of date

Quick start

cargo add composure --features macros,tier1
cargo add composure_adapter_cloudflare   # or composure_adapter_tokio
cargo add composure_sync                 # in a host-only `sync` binary

Or start from a template, with a Command, a Sync binary, a test, and the platform's configuration (a Dockerfile for tokio):

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

Then follow Getting started: it takes you from an empty crate to a deployed bot.

Crates

Crate
composure composure version The core: types, the handler runtime, the REST client, and the macros (macros feature)
composure_adapter_cloudflare composure_adapter_cloudflare version Serve on Cloudflare Workers
composure_adapter_tokio composure_adapter_tokio version Serve from a server on tokio and hyper
composure_sync composure_sync version Register your commands with Discord at deploy time
composure_macros composure_macros version The macros, re-exported by composure
composure_commands composure_commands version A builder for command definitions, without the macros

Everything optional is behind a feature flag (REST endpoints per resource, tracing, Web Crypto signature checks), so a Worker only ships what it uses. See feature flags.

Learn more

  • The guide: commands, responses, components and modals, Views and styling, REST and deferring, Webhook Events, platforms, testing. Every example in it is compiled and tested.
  • Examples: one bot using every feature, served on Workers and on tokio.
  • The Composure bot: Composure's own bot on Workers, a real deployment. Add it to get release announcements in your server (/notify setup), read the changelog (/changelog) and search these docs (/docs); /invite in any server it's in gives the install links.
  • Design decisions and the glossary.

Status

Composure is pre-1.0: the API may change between minor versions. It tracks Discord's v10 API and its latest components (Components V2, labels, radio groups, checkboxes, file uploads). Issues and pull requests are welcome, and questions go in the Discord server.

Contributing

See CONTRIBUTING.md for the development setup and how releases work: each pull request adds a change file (knope document-change), and merging the generated release PR publishes every crate. Every version is in the changelog.

License

Licensed under the Apache License, Version 2.0.