<div align="center">
<img src="https://raw.githubusercontent.com/BlueFrog130/composure-rs/main/assets/logo.svg" alt="Composure logo" width="128" height="128">
# 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.
[](https://crates.io/crates/composure)
[](https://docs.rs/composure)
[](https://github.com/BlueFrog130/composure-rs/actions/workflows/ci.yml)
[](https://crates.io/crates/composure)
[](https://github.com/BlueFrog130/composure-rs/blob/main/LICENSE)
[](https://blog.rust-lang.org/)
[](https://discord.com/invite/8rWjWtp3pG)
<br>
[](https://discord.com/developers/docs/interactions/overview)
[](https://workers.cloudflare.com/)
[](https://tokio.rs/)
[](https://webassembly.org/)
[](https://github.com/rust-secure-code/safety-dance/)
[Guide](https://bluefrog130.github.io/composure-rs/) ·
[API docs](https://docs.rs/composure) ·
[Examples](https://github.com/BlueFrog130/composure-rs/tree/main/examples) ·
[Discord](https://discord.com/invite/8rWjWtp3pG) ·
[Crates](#crates)
</div>
---
```rust
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](https://discord.com/developers/docs/interactions/overview)
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.
| 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]`
```rust
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](https://bluefrog130.github.io/composure-rs/views.html#tag-reference).
### Typed modal forms
```rust
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`
```rust
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
```rust
// 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
```sh
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
```sh
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):
```sh
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](https://bluefrog130.github.io/composure-rs/getting_started.html):
it takes you from an empty crate to a deployed bot.
## Crates
| [`composure`](https://crates.io/crates/composure) | [](https://crates.io/crates/composure) | The core: types, the handler runtime, the REST client, and the macros (`macros` feature) |
| [`composure_adapter_cloudflare`](https://crates.io/crates/composure_adapter_cloudflare) | [](https://crates.io/crates/composure_adapter_cloudflare) | Serve on Cloudflare Workers |
| [`composure_adapter_tokio`](https://crates.io/crates/composure_adapter_tokio) | [](https://crates.io/crates/composure_adapter_tokio) | Serve from a server on tokio and hyper |
| [`composure_sync`](https://crates.io/crates/composure_sync) | [](https://crates.io/crates/composure_sync) | Register your commands with Discord at deploy time |
| [`composure_macros`](https://crates.io/crates/composure_macros) | [](https://crates.io/crates/composure_macros) | The macros, re-exported by `composure` |
| [`composure_commands`](https://crates.io/crates/composure_commands) | [](https://crates.io/crates/composure_commands) | 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](https://docs.rs/composure/latest/composure/#feature-flags).
## Learn more
- **[The guide](https://bluefrog130.github.io/composure-rs/)**: commands,
responses, components and modals, Views and styling, REST and deferring, Webhook Events,
platforms, testing. Every example in it is compiled and tested.
- **[Examples](https://github.com/BlueFrog130/composure-rs/tree/main/examples)**: one bot using
every feature, served [on Workers](https://github.com/BlueFrog130/composure-rs/tree/main/examples/cloudflare)
and [on tokio](https://github.com/BlueFrog130/composure-rs/tree/main/examples/tokio).
- **[The Composure bot](https://github.com/BlueFrog130/composure-rs/tree/main/bots/composure)**:
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](https://github.com/BlueFrog130/composure-rs/tree/main/docs/adr)** and the
[glossary](https://github.com/BlueFrog130/composure-rs/blob/main/CONTEXT.md).
## 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](https://discord.com/invite/8rWjWtp3pG).
## Contributing
See [CONTRIBUTING.md](https://github.com/BlueFrog130/composure-rs/blob/main/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](https://github.com/BlueFrog130/composure-rs/blob/main/CHANGELOG.md).
## License
Licensed under the [Apache License, Version 2.0](https://github.com/BlueFrog130/composure-rs/blob/main/LICENSE).