Overview

twilight logo

Join us on Discord! :)

twilight is a powerful asynchronous, flexible, and scalable ecosystem of Rust libraries for the Discord API.

Check out the crates on crates.io.

Who Twilight is For

Twilight is meant for people who are very familiar with Rust and at least somewhat familiar with Discord bots. It aims to be the library you use when you want - or, maybe for scaling reasons, need - the freedom to structure things how you want and do things that other libraries may not strongly cater to.

If you're a beginner with Rust, then that's cool and we hope you like it! serenity is a great library for getting started and offers an opinionated, batteries-included approach to making bots. You'll probably have a better experience with it and we recommend you check it out.

The Guide

In this guide you'll learn about the core crates in the twilight ecosystem, useful first-party crates for more advanced use cases, and third-party crates giving you a tailored experience.

The organization for the project is on GitHub.

The crates are available on crates.io.

The API docs are also hosted for the latest version.

There is a community and support server on Discord.

A Quick Example

Below is a quick example of a program printing "Pong!" when a ping command comes in from a channel:

use std::{env, error::Error, sync::Arc};
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
use twilight_http::Client as HttpClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    let token = env::var("DISCORD_TOKEN")?;

    // Specify intents requesting events about things like new and updated
    // messages in a guild and direct messages.
    let intents = Intents::GUILD_MESSAGES | Intents::DIRECT_MESSAGES | Intents::MESSAGE_CONTENT;

    // Create a single shard.
    let mut shard = Shard::new(ShardId::ONE, token.clone(), intents);

    // The http client is separate from the gateway, so startup a new
    // one, also use Arc such that it can be cloned to other threads.
    let http = Arc::new(HttpClient::new(token));

    // Since we only care about messages, make the cache only process messages.
    let cache = DefaultInMemoryCache::builder()
        .resource_types(ResourceType::MESSAGE)
        .build();

    // Startup the event loop to process each event in the event stream as they
    // come in.
    while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
        let Ok(event) = item else {
            tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

            continue;
        };
        // Update the cache.
        cache.update(&event);

        // Spawn a new task to handle the event
        tokio::spawn(handle_event(event, Arc::clone(&http)));
    }

    Ok(())
}

async fn handle_event(
    event: Event,
    http: Arc<HttpClient>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
    match event {
        Event::MessageCreate(msg) if msg.content == "!ping" => {
            http.create_message(msg.channel_id).content("Pong!").await?;
        }
        Event::Ready(_) => {
            println!("Shard is ready");
        }
        _ => {}
    }

    Ok(())
}

Support

The guide, and Twilight as a whole, assume familiarity with Rust, Rust's asynchronous features, and bots in general. If you're new to Rust and/or new to bots, consider checking out serenity, which is a beginner-friendly, batteries-included approach to the Discord API.

Support for the library is provided through the GitHub issues section and the Discord server.

If you have a question, then the issues or the server are both good fits for it. If you find a bug, then the issues section is the best place.

The API documentation is also available.

Supported Rust Versions

Twilight currently supports an MSRV of Rust 1.53+.

Breaking Changes

Although Twilight aims to design APIs right the first time, that obviously won't happen. A lot of effort is spent designing clear and correct interfaces.

While Twilight takes care to avoid the need for breaking changes, it will be fearless when it needs to do so: they won't be avoided for the sake of avoiding a change. Breaking changes won't be piled up over time to make a single big release: major versions will be often and painless.

Crates

Twilight is, at heart, an ecosystem. These components of the ecosystem don't depend on each other in unnecessary ways, allowing you to pick and choose and combine the crates that you need for your use case. The crates for Twilight are categorised into three groups: the core crates, first-party crates, and third-party crates.

Core Crates

Twilight includes a few crates which are the "building blocks" to most peoples' use cases. You might not need them all, but generally speaking you'll need most of them. Most of them wrap Discord's various APIs.

  • model: All of the structs, enums, and bitflags used by the Discord APIs.
  • http: HTTP client supporting all of the documented features of Discord's HTTP API, with support for ratelimiting, proxying, and more.
  • gateway: Clients supporting Discord's gateway API.
  • cache: Definitions for implementing a cache. An in-process memory implementation is included.
  • standby: Utility for asynchronously waiting for certain events, like a new message in a channel or a new reaction to a message.
  • util: Provides various utilities for use with twilight such as: builders for larger structs, permissing calculator to calculate permission of members and various extension traits for snowflakes.

First-Party Crates

There are some first-party crates maintained by the Twilight organization, but not included in the core experience. These might be for more advanced or specific use cases or clients for third-party services. An example of a first-party crate is twilight-lavalink, a Client for interacting with Lavalink.

Third-Party Crates

Third-party crates are crates that aren't officially supported by the Twilight organization, but are recognised by it.

Model

twilight-model is a crate of models for use with serde defining the Discord APIs with limited implementations on top of them.

These are in a single crate for ease of use, a single point of definition, and a sort of versioning of the Discord API. Similar to how a database schema progresses in versions, the definition of the API also progresses in versions.

Most other Twilight crates use types from this crate. For example, the Embed Builder crate primarily uses types having to do with channel message embeds, while the Lavalink crate works with a few of the events received from the gateway. These types being in a single versioned definition is beneficial because it removes the need for crates to rely on other large and unnecessary crates.

The types in this crate are reproducible: deserializing a payload into a type, serializing it, and then deserializing it again will result in the same instance.

Defined are a number of modules defining types returned by or owned by resource categories. For example, gateway contains types used to interact with and returned by the gateway API. guild contains types owned by the Guild resource category. These types may be directly returned by, built on top of, or extended by other Twilight crates.

Id

The Id type has a marker depending on which context it is used in. In most cases it will be possible for the compiler to infer the marker that it needs to have. This helps ensuring that IDs are not used in the wrong context. If you need to use the id in a different context you can use the cast method which allow changing the marker type. This is for example helpful to turn a guild ID into a role ID to get the @everyone role. It comes with no additional run-time cost.

Example

use twilight_model::id::{Id, marker::{GuildMarker, RoleMarker}};
fn main() {

let guild_id: Id<GuildMarker> = Id::new(123);

// To get the everyone role we have to convert the guild id to a role id.
let everyone_role_id: Id<RoleMarker> = guild_id.cast();
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-model

docs: https://docs.rs/twilight-model

crates.io: https://crates.io/crates/twilight-model

HTTP

twilight-http is an HTTP client wrapping all of the documented Discord HTTP API. It is built on top of hyper, and allows you to pick your own TLS backend. By default, it uses RusTLS a Rust TLS implementation, but it can be changed to use NativeTLS, which uses the TLS native to the platform, and on Unix uses OpenSSL.

Ratelimiting is included out-of-the-box, along with support for proxies.

Features

Deserialization

twilight-gateway supports serde_json and simd-json for deserializing and serializing events.

SIMD

The simd-json feature enables usage of simd-json, which uses modern CPU features to more efficiently deserialize JSON data. It is not enabled by default.

In addition to enabling the feature, you will need to add the following to your <project_root>/.cargo/config:

[build]
rustflags = ["-C", "target-cpu=native"]

TLS

twilight-http has features to enable certain HTTPS TLS connectors.

These features are mutually exclusive. rustls is enabled by default.

Native-TLS

The native-tls feature causes the client to use hyper-tls. This will use the native TLS backend, such as OpenSSL on Linux.

RusTLS

The rustls feature causes the client to use hyper-rustls. This enables usage of the RusTLS crate as the TLS backend.

This is enabled by default.

Example

A quick example showing how to get the current user's name:

use std::{env, error::Error};
use twilight_http::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    // Initialize the tracing subscriber.
    tracing_subscriber::fmt::init();

    let client = Client::new(env::var("DISCORD_TOKEN")?);

    let me = client.current_user().await?.model().await?;
    println!("Current user: {}#{}", me.name, me.discriminator);

    Ok(())
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-http

docs: https://docs.rs/twilight-http

crates.io: https://crates.io/crates/twilight-http

Gateway

twilight-gateway is an implementation of a client over Discord's websocket gateway.

The main type is the Shard: it connects to the gateway, receives messages, parses and processes them, and then gives them to you. It will automatically reconnect, resume, and identify, as well as do some additional connectivity checks.

Features

twilight-gateway includes a number of features for things ranging from payload deserialization to TLS features.

Deserialization

twilight-gateway supports serde_json and simd-json for deserializing and serializing events.

SIMD

The simd-json feature enables usage of simd-json, which uses modern CPU features to more efficiently deserialize JSON data. It is not enabled by default.

In addition to enabling the feature, you will need to add the following to your <project_root>/.cargo/config:

[build]
rustflags = ["-C", "target-cpu=native"]

TLS

twilight-gateway has features to enable tokio-websockets' TLS features. These features are mutually exclusive. rustls-native-roots is enabled by default.

Native-TLS

The native-tls feature enables tokio-websockets' native-tls feature.

RusTLS

RusTLS allows specifying from where certificate roots are retrieved from.

Native roots

The rustls-native-roots feature enables tokio-websockets' rustls-native-roots feature.

This is enabled by default.

Web PKI roots

The rustls-webpki-roots feature enables tokio-websockets' rustls-webpki-roots feature.

Zlib

Stock

The zlib-stock feature makes flate2 use of the stock Zlib which is either upstream or the one included with the operating system.

SIMD

zlib-simd enables the use of zlib-ng which is a modern fork of zlib that in most cases will be more effective. However, this will add an external dependency on cmake.

If both are enabled or if the zlib feature of flate2 is enabled anywhere in the dependency tree it will make use of that instead of zlib-ng.

Example

Starting a Shard and printing the contents of new messages as they come in:

use std::{env, error::Error};
use twilight_gateway::{EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    // Initialize the tracing subscriber.
    tracing_subscriber::fmt::init();

    let token = env::var("DISCORD_TOKEN")?;
    let intents = Intents::GUILD_MESSAGES;
    let mut shard = Shard::new(ShardId::ONE, token, intents);
    tracing::info!("created shard");

    while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
        let Ok(event) = item else {
            tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

            continue;
        };

        tracing::debug!(?event, "event");
    }

    Ok(())
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-gateway

docs: https://docs.rs/twilight-gateway

crates.io: https://crates.io/crates/twilight-gateway

Cache

Twilight includes an in-process-memory cache. It's responsible for processing events and caching things like guilds, channels, users, and voice states.

Examples

Process new messages that come over a shard into the cache:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::env;
use twilight_cache_inmemory::DefaultInMemoryCache;
use twilight_gateway::{EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};

let token = env::var("DISCORD_TOKEN")?;

let mut shard = Shard::new(ShardId::ONE, token, Intents::GUILD_MESSAGES);

let cache = DefaultInMemoryCache::new();

while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
    let Ok(event) = item else {
        tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

        continue;
    };

    cache.update(&event);
}
    Ok(())
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-cache-inmemory

docs: https://docs.rs/twilight-cache-inmemory

crates.io: https://crates.io/crates/twilight-cache-inmemory

Standby

Standby is a utility to wait for an event to happen based on a predicate check. For example, you may have a command that makes a reaction menu of ✅ and ❌. If you want to handle a reaction to these, using something like an application-level state or event stream may not suit your use case. It may be cleaner to wait for a reaction inline to your function. This is where Standby comes in.

Examples

Wait for a message in channel 123 by user 456 with the content "test":

#[allow(unused_variables)]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use twilight_model::{
    gateway::payload::incoming::MessageCreate,
    id::Id,
};
use twilight_standby::Standby;

let standby = Standby::new();

// Later on in the application...
let message = standby
    .wait_for_message(
        Id::new(123),
        |event: &MessageCreate| {
            event.author.id == Id::new(456) && event.content == "test"
        },
    )
    .await?;
    Ok(())
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-standby

docs: https://docs.rs/twilight-standby

crates.io: https://crates.io/crates/twilight-standby

First-party

Twilight includes crates maintained by the organization, but not included as part of the core experience. Just like all of the core crates these are entirely opt-in, but are for more advanced or specific use cases, such as integration with other software.

Although not a part of the core experience, these are given the same level of support as the core crates.

Mention

twilight-mention is a utility crate to mention model resources.

With this library, you can create mentions for various resources, such as users, emojis, roles, members, or channels.

Examples

Create a mention formatter for a user ID, and then format it in a message:

#[allow(unused_variables)]
fn main() {
use twilight_mention::Mention;
use twilight_model::id::{Id, marker::UserMarker};

let user_id: Id<UserMarker> = Id::new(123);
let message = format!("Hey there, {}!", user_id.mention());
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-mention

docs: https://docs.rs/twilight-mention

crates.io: https://crates.io/crates/twilight-mention

Lavalink

twilight-lavalink is a client for Lavalink for use with model events from the gateway.

It includes support for managing multiple nodes, a player manager for conveniently using players to send events and retrieve information for each guild, and an HTTP module for creating requests using the http crate and providing models to deserialize their responses.

Features

HTTP Support

The http-support feature adds types for creating requests and deserializing response bodies of Lavalink's HTTP routes via the http crate.

This is enabled by default.

TLS

twilight-lavalink has features to enable tokio-websockets' TLS features. These features are mutually exclusive. rustls-native-roots is enabled by default.

Native-TLS

The native-tls feature enables tokio-websockets' native-tls feature.

RusTLS

RusTLS allows specifying from where certificate roots are retrieved from.

Native roots

The rustls-native-roots feature enables tokio-websockets' rustls-native-roots feature.

This is enabled by default.

Web PKI roots

The rustls-webpki-roots feature enables tokio-websockets' rustls-webpki-roots feature.

Examples

Create a client, add a node, and give events to the client to process events:

use std::{
    env,
    error::Error,
    net::SocketAddr,
    str::FromStr,
};
use twilight_gateway::{EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
use twilight_http::Client as HttpClient;
use twilight_lavalink::Lavalink;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
    let token = env::var("DISCORD_TOKEN")?;
    let lavalink_host = SocketAddr::from_str(&env::var("LAVALINK_HOST")?)?;
    let lavalink_auth = env::var("LAVALINK_AUTHORIZATION")?;
    let shard_count = 1_u32;

    let http = HttpClient::new(token.clone());
    let user_id = http.current_user().await?.model().await?.id;

    let lavalink = Lavalink::new(user_id, shard_count);
    lavalink.add(lavalink_host, lavalink_auth).await?;

    let intents = Intents::GUILD_MESSAGES | Intents::GUILD_VOICE_STATES;
    let mut shard = Shard::new(ShardId::ONE, token, intents);

    while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
        let Ok(event) = item else {
            tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

            continue;
        };

        lavalink.process(&event).await?;
    }

    Ok(())
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-lavalink

docs: https://docs.rs/twilight-lavalink

crates.io: https://crates.io/crates/twilight-lavalink

Util

twilight-util is a utility crate that adds utilities to the twilight ecosystem that do not fit in any other crate. One example feature of the crate is a trait to make extracting data from Discord identifiers (snowflakes) easier.

Features

twilight-util by default exports nothing. Features must be individually enabled via feature flags.

Builder

The builder feature enables builders for large structs. At the time of writing, it contains the following builders:

Command example

Create a command that can be used to send a animal picture in a certain category:

fn main() {
use twilight_model::application::command::CommandType;
use twilight_util::builder::command::{BooleanBuilder, CommandBuilder, StringBuilder};

CommandBuilder::new(
    "blep",
    "Send a random adorable animal photo",
    CommandType::ChatInput,
)
.option(
    StringBuilder::new("animal", "The type of animal")
        .required(true)
        .choices([
            ("Dog", "animal_dog"),
            ("Cat", "animal_cat"),
            ("Penguin", "animal_penguin"),
        ]),
)
.option(BooleanBuilder::new(
    "only_smol",
    "Whether to show only baby animals",
));
}

Embed examples

Build a simple embed:

#[allow(unused_variables)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use twilight_util::builder::embed::{EmbedBuilder, EmbedFieldBuilder};

let embed = EmbedBuilder::new()
    .description("Here's a list of reasons why Twilight is the best pony:")
    .field(EmbedFieldBuilder::new("Wings", "She has wings.").inline())
    .field(EmbedFieldBuilder::new("Horn", "She can do magic, and she's really good at it.").inline())
    .build();
    Ok(())
}

Build an embed with an image:

#[allow(unused_variables)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use twilight_util::builder::embed::{EmbedBuilder, ImageSource};

let embed = EmbedBuilder::new()
    .description("Here's a cool image of Twilight Sparkle")
    .image(ImageSource::attachment("bestpony.png")?)
    .build();
    Ok(())
}

The link feature enables the parsing and formatting of URLs to resources, such as parsing and formatting webhook links or links to a user's avatar.

Examples

Parse a webhook URL with a token:

#[allow(unused_variables)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use twilight_model::id::Id;
use twilight_util::link::webhook;

let url = "https://discord.com/api/webhooks/794590023369752587/tjxHaPHLKp9aEdSwJuLeHhHHGEqIxt1aay4I67FOP9uzsYEWmj0eJmDn-2ZvCYLyOb_K";

let (id, token) = webhook::parse(url)?;
assert_eq!(Id::new(794590023369752587), id);
assert_eq!(
    Some("tjxHaPHLKp9aEdSwJuLeHhHHGEqIxt1aay4I67FOP9uzsYEWmj0eJmDn-2ZvCYLyOb_K"),
    token,
);
Ok(()) }

Permission Calculator

The permission-calculator feature is used for calculating the permissions of a member in a channel, taking into account its roles and permission overwrites.

Snowflake

The snowflake feature calculates information out of snowflakes, such as the timestamp or the ID of the worker that created it.

Examples

Retrieve the timestamp of a snowflake in milliseconds from the Unix epoch as a 64-bit integer:

#[allow(unused_variables)]
fn main() {
use twilight_util::snowflake::Snowflake;
use twilight_model::id::{Id, marker::UserMarker};

let user: Id<UserMarker> = Id::new(123456);
let timestamp = user.timestamp();
}

source: https://github.com/twilight-rs/twilight/tree/main/twilight-util

docs: https://docs.rs/twilight-util

crates.io: https://crates.io/crates/twilight-util

Gateway queue

twilight-gateway-queue is a trait and some implementations that are used by the gateway to ratelimit identify calls. Developers should prefer to use the re-exports of these crates through the gateway.

source: https://github.com/twilight-rs/twilight/tree/main/twilight-gateway-queue

docs: https://docs.rs/twilight-gateway-queue

crates.io: https://crates.io/crates/twilight-gateway-queue

Third-party

Third-party crates are crates that aren't supported by the Twilight organization but are recognised by it. Of course, use these at your own risk. :)

Third-party crates may become first-party crates if they end up becoming useful enough for a large number of users.

List of Crates

Below is a list of crates. If you want yours added, feel free to ask!

baptiste0928/twilight-interactions

twilight-interactions provides macros and utilities to make interactions easier to use. Its features include slash command parsing and creation from structs with derive macros.

GitHub repository - Documentation

Vesper Framework

vesper is a slash command framework meant to be used with twilight. It uses procedural macros to make implementing slash commands as easy as possible and provides flexible argument parsing. Modals are also supported making use of derive macros, so they can be used effortlessly.

Github repository - Documentation

Services

Twilight is built with a service-minded approach. This means that it caters to both monolithic and multi-serviced applications equally. If you have a very large bot and have a multi-serviced application and feel like Rust is a good language to use for some of your services, then Twilight is a great choice. If you have a small bot and just want to get it going in a monolithic application, then it's also a good choice. It's easy to split off parts of your application into other services as your application grows.

Gateway groups

One of the popular design choices when creating a multi-serviced application is to have a service that only connects shards to the gateway and sends the events to a broker to be processed. As bots grow into hundreds or thousands of shards, multiple instances of the application can be created and groups of shards can be managed by each. Twilight is an excellent choice for this use case: just receive and send the payloads to the appropriate broker stream. Twilight shards need only partially deserialize payloads to function.

Gateway session ratelimiting

If multiple shard groups are used, then they need to be queued and their session initialization ratelimited. The gateway includes a Queue trait which can be implemented; the gateway will submit a request to the queue before starting a session. Twilight comes with a queue that supports Large Bot sharding, but when multiple shard groups are in use then a custom queue will need to be implemented. Refer to gateway-queue for an example of this.

HTTP proxy ratelimiting

If you have multiple services or lambda functions that can make HTTP requests, then you'll run into ratelimiting issues. Twilight's HTTP client supports proxying, and can be combined with something like our very own http-proxy to proxy requests and ratelimit them.

The sky is the limit

You can do so much more than just this, and that's the beauty of the ecosystem: it's flexible enough to do anything you need, and if you find something it can't then we'll fix it. The goal is to remove all limitations on designs and allow you to do what you need.

Bots using Twilight

Below is a list of bots known to be using the Twilight ecosystem. The use could be as small as only the gateway or HTTP client, or as large as all of the core crates.

Want your bot added? Feel free to send a PR to the repo!

Open-Source

Gearbot

The GearBot team are rewriting their bot to use Twilight, with a need for performance and scalability in mind.

Source: GitHub

Lasagne bot

Lasagne bot is a bot that posts garfield comics.

Source: Sr.ht

HarTex

HarTex is a Discord bot built and optimized for server administration and moderation needs in mind.

Source: GitHub

interchannel message mover

a discord bot to move messages between channels easily

Source: GitHub

Timezoner

Timezoner is a bot that lets people send a date/time that appears in everyone's own timezone.

Source: GitHub

Tricked-Bot

A invite tracker and autoresponder bot.

Source: Github

Version History

Version 0.15 - 2023-02-05

Version 0.15 of the Twilight ecosystem brings a new implementation of the Gateway undertaken over the last year, with quality of life improvements and bugfixes made in other areas.

With the new gateway implementation finer controls over shards, improved performance, and new documentation have been introduced. Although overall usage of the gateway crate is not very dissimilar from existing usage for most use cases, the core event loop and setting up of shards is different. Besides the gateway, a number of quality of life improvements have been made in the model crate, with a sprinkling of bugfixes across the ecosystem.

New Gateway

We have rewritten the internals of the gateway from scratch, with focuses on three key areas: performance, control, and simplicity. In the pursuit of performance, the model of awaiting a stream of events from a background task has been shelved in favor of direct asynchronous polling. Essentially, background channels and tasks have been removed, and thus removing layers of asynchronous tasks depending on each other. Everyone always wants to control more with the APIs they're provided, which is why we've dedicated time to making the gateway API extensible and concise, yet powerful. Receiving websocket messages, manual message payloads, manual control of groups of shards, and more is now possible. Being able to understand the implementation when debugging a problem is vital, which is why we've simplified the implementation. The control flow has been significantly simplified and documented, while the size of implementation has been slimmed down by 30%.

Shards

The core usage of a shard is not very dissimilar. While creating a shard without specialized configuration is still done via Shard::new, creating a shard with specialized configuration is now done via the ConfigBuilder and Shard::with_config. An stream of gateway events is no longer returned along with the new shard; instead of awaiting Events::next in a loop, Shard::next_event can be awaited in a loop. Let's take a look at how basic usage of a shard has changed:

Twilight 0.14
let intents = Intents::GUILDS | Intents::GUILD_MODERATION;
let (shard, mut events) = Shard::new(env::var("DISCORD_TOKEN")?, intents);

shard.start().await?;
println!("Created shard");

while let Some(event) = events.next().await {
    println!("Event: {event:?}");
}

Twilight 0.15
use std::{env, error::Error};
use twilight_gateway::{Intents, Shard, ShardId};

#[tokio::main] async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let intents = Intents::GUILDS | Intents::GUILD_MODERATION;
let mut shard = Shard::new(ShardId::ONE, env::var("DISCORD_TOKEN")?, intents);

loop {
    let event = match shard.next_event().await {
        Ok(event) => event,
        Err(source) => {
            tracing::warn!(?source, "error receiving event");

            if source.is_fatal() {
                break;
            }

            continue;
        }
    };

    println!("Event: {event:?}");
}
Ok(()) }

Notably, receiving and sending messages now require a mutable reference to the shard instance, as opposed to Twilight 0.14 which only required an immutable reference. This makes sharing a reference to a shard across tasks for sending messages and accessing shard information — such as a shard's status or its configuration — impossible to achieve the same way as with Twilight 0.14. Instead of sharing the shard itself to spawned tasks it's recommended to provide necessary information to tasks when they are spawned, or maintaining a mutex of necessary shard information that is passed around to tasks.

Sending messages — such as member chunk requests or presence updates — over the shard from spawned tasks is now different: instead of being able to directly send a message (0.14), a message sender can be obtained and passed to tasks. This will allow the sending of messages from spawned tasks to the shard, to then be sent to Discord's gateway.

These are the primary changes to shards! Some new additions have been made: the message ratelimiter can now be accessed, statistics about the message inflater can now be accessed, and the gateway session and connection latency are more concise.

Clusters

Twilight 0.14 had an API surface on top of shards: clusters. Clusters were essentially a wrapper over shards with the intention of being used for managing multiple shards within one type. The Cluster API duplicated most of the shard API, with an equivalent event stream that wrapped multiple shards' streams, a Cluster type that instantiated and owned multiple shards with methods mostly equvialent to shards' methods, and errors wrapping shard errors.

With Twilight 0.15 the concept of a "cluster" has largely been done away with and replaced by the stream module. Our aim with this change was to create transparency about what is happening under the hood, reduce the API surface, and reduce complexity.

The module contains three functions for creating groups of shards:

  • create_recommended to create the number of shards Discord recommends;
  • create_range to create the shards within a range; and
  • create_bucket to create the shards within a bucket, necessary for very large bot sharding.

These functions all return an iterator of shards. Implementing loops to receive events from this group of shards can be difficult, so we've provided two types for collecting shards and efficiently polling all of them:

Let's take a look at what starting a range of shards and iterating over their events looks like:

Loop over the events of a group of shards
use futures::StreamExt;
use std::{env, error::Error};
use twilight_gateway::{
    stream::{self, ShardEventStream},
    Config,
    Intents,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    // Initialize the tracing subscriber.
    tracing_subscriber::fmt::init();

    let token = env::var("DISCORD_TOKEN")?;
    let config = Config::new(token, Intents::GUILD_MESSAGES);

    // Create a group of shards with IDs 0 through 10, out of a total of 20
    // shards.
    let mut shards = stream::create_range(
        0..10,
        20,
        config,
        |_, builder| builder.build(),
    ).collect::<Vec<_>>();

    // Create a stream to collect all of the shards and poll them for their next
    // Discord gateway events.
    let mut stream = ShardEventStream::new(shards.iter_mut());

    while let Some((shard, event)) = stream.next().await {
        let event = match event {
            Ok(event) => event,
            Err(source) => {
                tracing::warn!(?source, "error receiving event");

                // An error may be fatal when something like invalid privileged
                // intents are specified or the Discord token is invalid.
                if source.is_fatal() {
                    break;
                }

                continue;
            }
        };

        tracing::debug!(?event, shard = ?shard.id(), "received event");
    }

    Ok(())
}

In each iteration the next received event and the shard that produced the event are returned. Implementing this kind of stream manually is somewhat trivial to do, but there are some hidden aspects that make this API particularly efficient. The shard is a mutable reference to the shard. When an iteration is over and it loops back over, the shard is re-inserted into the stream. Because the stream is never re-created, the futures polling shards aren't re-created on each loop. This allows for a constant and fast iteration over shards.

We hope that this thin yet powerful layer over shards will allow for a greater level of flexibility while not being cumbersome to use. Be sure to check out the documentation to see the full picture of how the gateway looks. If you have questions about how to migrate your application to the new Gateway, please ask in the #support channel in our Discord server or in our GitHub Discussions!

Token Debugging

Previous versions of Twilight derived Debug on a few types that contain tokens, such as the HTTP crate's Client and the gateway's Shard. Twilight has taken the step to manually derive Debug on types containing tokens to prevent tokens from being printed in logs. A small but important improvement for security!

Removal of Guild IDs on Members

Member models have always had the ID of the guild the user is a part of stored on them. Discord doesn't actually send the guild ID as part of member objects. Twilight has always injected the guild ID into members as an ergonomic improvement because guild IDs have always been in context when deserializing or retrieving members, such as in MemberChunk events or when fetching a guild's member list. Because Twilight aims to map the Discord API 1:1 as closely as possible, we've taken the step to remove guild IDs from members.

When working with members a guild ID should usually be known. For example, the guild ID is present in the MemberAdd event and is required along with the user ID when fetching a member from the cache.

In the future, one case where a guild ID won't be known is when fetching the guild member details about the members of a channel thread. This is because only the channel ID is known, and a guild ID isn't returned. This problem was a motivating factor for this change. Check out issue #2058 for more information.

Command Option Choice Refactoring

CommandOptionChoices have been refactored. They were previously an enum with variants for each type of choice (integers, numbers, and strings). In turn, these variants contained a data struct with the name, localized names, and value of the choice. We've simplified these definitions by making CommandOptionChoice a struct containing the name and localized names, with the value field being the enum with variants for each type of value. This allows for direct access of a choice's names.

Guild Widget Settings Support

Fetching information about a guild widget and updating its settings has always been supported, but last year Discord documented support for fetching the settings of a guild widget. We've introduced support for this via the new GetGuildWidgetSettings request. This returns whether the widget is enabled and the channel ID the widget points to.

Allowed Mentions

AllowedMentions has seen a small touchup. While its documentation has been greatly improved, ParseTypes (0.14) has been renamed to the clearer MentionType. The builder for allowed mentions has been removed and may be brought back into the utilities crate in the future.

AFK Timeouts

Guild::afk_timeouts are now stored as the new AfkTimeout instead of as an integer. This allows use of valid values of AFK timeouts, and implements a conversion into a Duration. Neat! AfkTimeout has a getter for accessing the raw integer, AfkTimeout::get.

2023-09-10 Updates

We've published minor versions of a number of crates. Most of these are related to support for new Discord API features, but a lot of bugfixes are included as well. We are also increasing our MSRV.

Bugfixes

twilight-gateway will no longer swallow messages when an I/O error is encountered during the reply to the message. It further will no longer attempt to send commands before a resume has been sent upon reconnecting and also attempt to resume any pending identify request.

UpdateFollowup's body type has been corrected to a Message instead of an empty body.

The validation of CommandOptions and the scheduled event get users limit in twilight-validate was incorrect and has been adjusted.

Using GetInvite's with_counts and with_expiration methods resulted in incorrect query strings being generated, this has been fixed.

Deprecations

ConfigBuilder::with_config has been deprecated in favor of a From<Config> implementation on ConfigBuilder.

Documentation Improvements

An accidental half-sentence in Client::delete_messages' documentation was removed.

Broken intra-doc links in choice_name were fixed.

twilight-http's Client::guild_members documentation was not setting the member limit mentioned in the documentation, it now matches the description.

Feature Additions

Discord's new username system is now supported and User::global_name has been added.

Guild onboarding is now supported and respective models and routes have been added to twilight-model and twilight-http: Client::guild_onboarding and GetGuildOnboarding in twilight-http; Onboarding, OnboardingPrompt, OnboardingPromptEmoji, OnboardingPromptOption, OnboardingPromptType, OnboardingPromptMarker and OnboardingPromptOptionMarker in twilight-model.

Join raid and mention raid protection are now supported in twilight-model and twilight-cache-in-memory. Guild::safety_alerts_channel_id, AutoModerationTriggerMetadata::mention_raid_protection_enabled and GuildFeature::RaidAlertsDisabled were added.

The Role::flags field is now supported and the RoleFlags enum has been added.

The Attachment::flags field is now supported and the AttachmentFlags enum has been added.

AuditLogOptionalEntryInfo has a new field, integration_type, for the type of integration that performed the action. The missing GuildIntegrationType::GuildSubscription variant has been added.

The GatewayReaction::message_author_id field which is included in ReactionAdd events was added.

The USE_EXTERNAL_SOUNDS permission is now supported.

AuditLogEventType was extended to include the monetization event types, CreatorMonetizationRequestCreated and CreatorMonetizationTermsAccepted.

It is now possible to set a default forum layout during channel creation via CreateGuildChannel::default_forum_layout.

The ThreadMemberUpdate event now includes the guild_id.

Event::guild_id has been extended to return more events' guild ID.

The twilight-model internal IdVisitor now implements visit_i64, allowing deserialization of Id types from integers using simd-json.

twilight-model no longer depends on tracing.

MSRV

All twilight ecosystem crates now target a MSRV of rustc 1.67. This was necessary due to MSRV increases in our dependencies.

Performance Improvements

The gateway ratelimiter now re-uses its cleanup instant, thus removing a syscall.

2023-04-27 Updates

We've published minor versions of a number of crates. This release is composed of bugfixes, new features, performance improvements, documentation improvements, and dependency updates.

Bugfixes

Discord's Clyde AI bot has a unique avatar hash. Unlike other hashes that are hex based, Clyde's avatar hash is simply "clyde". We now handle deserialization of Clyde's avatar in our ImageHash optimization.

Application command interaction options of type String are no longer trimmed of leading zeroes.

The UpdateCurrentMember request now correctly removes the current member's nickname if passed None.

The gateway queue's DayLimiter now properly calculates when the session resets.

The UpdateGuildSticker request now uses the specified audit log reason; prior, it was accidentally ignoring the reason.

Dependency Updates

The allowed version range of simd-json was broadened to >=0.4, <0.10 in twilight-gateway and twilight-http.

Documentation Improvements

Standby now has an example of how to timeout futures and streams.

The Event::IntegrationDelete and Event::IntegrationUpdate variants' documentation was inversed, and has now been corrected.

Feature Additions

The AutoModerationTriggerMetadata struct now supports the mention_total_limit and regex_patterns fields.

The guild Permissions bitflag now supports the VIEW_CREATOR_MONETIZATION_ANALYTICS and USE_SOUNDBOARD variants and renames MANAGE_EMOJIS_AND_STICKERS to MANAGE_GUILD_EXPRESSIONS.

The Interaction struct now supports the channel field.

Performance Improvements

The gateway's CommandRatelimiter performance has been improved by over 98%, with common calls being reduced from around 4 microseconds to around 57 nanoseconds. This is something that can be used by users, and is also used by shards when sending commands, making common operations just a bit more speedy. Impressive!

Avatars, banners, icons, and other assets are received as hashes, which we have ImageHash for as a performance improvement in storage. Instead of storing hashes as heap-allocated strings, we store them as 16 bytes on the stack. The performance of the deserialization and parsing of hashes is now 38% faster.

Caching users received in InteractionCreate events is now a bit faster in some situations due to keying into a HashMap to check for the existence of a user instead of iterating over the HashMap.

2023-02-26 Updates

We've published minor versions of a number of crates. In a recent campaign to improve documentation when support tickets are received in our Discord server, these releases largely contain improved documentation. The remainder of the releases are dedicated to new Discord API features.

Get Thread Members Request Pagination

The GetThreadMembers HTTP request now supports pagination via the usual after and limit methods. It defaults to returning a subset of information about the users in a given thread, but supports specifying whether to retrieve the full member details of those users.

Unknown Shard Event Behavior

Gateway shards allow receiving Websocket messages or the next Gateway event. Discord sends events that are undocumented, which Twilight doesn't support. Additionally, there may be new events Twilight doesn't immediately support. However, an error would be returned when an unimplemented event is encountered due to a parsing error. We've changed iteration over events to ignore unknown events. Iterating over websocket messages and parsing them via twilight_gateway::parse should be preferred when an event Twilight doesn't support is required.

Message Notification Suppression

Notifications about messages can now be suppressed via a new message flag, SUPPRESS_NOTIFICATIONS. It can be specified in the CreateMessage and CreateForumThreadMessage HTTP requests.

Stage Channel Message Types

Discord launched text-in-stage, which is much like text-in-voice. It includes four new message types, with all being system messages:

Custom AutoMod Block Messages

Discord recently launched custom messages for AutoMod block message actions. Custom messages can be added to rules via CreateAutoModerationRule::action_block_message_with_explanation.

Group OAuth2 Management

Channels have a new field called managed, which specifies whether a group is managed by an application via the gdm.join OAuth2 scope.

Gateway OpCode Categorization

Helpful for those working with the lower levels of the gateway, the gateway OpCode type has two new methods: is_received and is_sent. These can be used to determine whether an OpCode is meant to be received or sent. While many OpCodes can either be received or sent, some can be received and sent, so the categorization can be helpful.

Version 0.16 - 2024-??-??

❗ This is a pre-release version

Version 0.16 of the Twilight ecosystem brings a lot of internal changes and refactors, the latest versions of all dependencies and catches up with new API features added by Discord. It also contains a couple of bugfixes.

Feature name changes

The native feature in all crates that had one was renamed to native-tls to avoid potential misconceptions about its purpose. Similarly, the trust-dns feature exposed in HTTP was renamed to hickory to account for the project's rebranding.

Generic in-memory cache

Our in-memory cache implementation is now generic, meaning you can now write custom cached representations of all the models. There are a couple of trait bounds that need to be met for the models, however. The new cache-optimization example demonstrates how to write your own cache models and implement the traits. These changes will let you drop fields that you don't need to store for your bot to function and save on memory.

Since InMemoryCache is now a generic type, existing code will have to be updated to instead use DefaultInMemoryCache, which is a drop-in replacement for the old type.

Gateway queue rewrite

The gateway queue crate was rewritten from scratch. The Queue trait no longer returns an opaque future type, instead it makes use of channels now.

The three separate queue implementations were merged into one, the InMemoryQueue. It is recommended to fetch the data for instantiating one from the Discord API via Client::gateway to avoid getting ratelimited.

The old NoOpQueue can be replicated by setting max_concurrency to 0.

Gateway refactors

The gateway crate has seen several changes as well. Alongside the gateway queue rewrite, the Queue on the shards is now stored as a generic to avoid an allocation. It defaults to an InMemoryQueue.

A major pitfall with twilight's gateway pre-0.16 was that Shard::next_event and Shard::next_message were not cancellation-safe. This has been addressed by implementing Stream for the shard and updating the internals to be cancellation-safe. futures_util::StreamExt::next now serves as the replacement for Shard::next_message, while twilight_gateway::StreamExt::next_event replaces Shard::next_event.

Additionally, the Config struct now no longer stores the EventTypeFlags, those have to be passed to twilight_gateway::StreamExt::next_event now.

The Shard::command, Shard::send and Shard::close methods now also queue their action into a channel, like MessageSender, and are therefore no longer async and now infallible.

The create_range method was renamed to create_iterator and takes an iterator over shard IDs instead of ranges. The create_* methods were also moved to the top of the crate.

We also reworked the error types. ProcessError was removed entirely, while SendError was renamed to ChannelError. ReceiveMessageErrorType now only has four variants.

The ConnectionStatus enum was renamed to ShardState and its Connected variant to Active. The close code is no longer stored and a few methods were removed. Analogously, the method to retrieve it was renamed to Shard::state.

In order to protect against future API changes, the parse method no longer errors upon encountering unknown events.

Putting it all together, the basic example of iterating over all events for a single shard now looks like this:

Twilight 0.15
let intents = Intents::GUILDS | Intents::GUILD_MODERATION;
let mut shard = Shard::new(ShardId::ONE, env::var("DISCORD_TOKEN")?, intents);

loop {
    let event = match shard.next_event().await {
        Ok(event) => event,
        Err(source) => {
            tracing::warn!(?source, "error receiving event");

            if source.is_fatal() {
                break;
            }

            continue;
        }
    };

    println!("Event: {event:?}");
}

Twilight 0.16
use twilight_gateway::StreamExt;

let intents = Intents::GUILDS | Intents::GUILD_MODERATION;
let mut shard = Shard::new(ShardId::ONE, env::var("DISCORD_TOKEN")?, intents);

while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
    let Ok(event) = item else {
        tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

        continue;
    };

    println!("Event: {event:?}");
}

HTTP errors

The HTTP request builders now return their errors upon finalization instead of each stage of the building process. The validation errors previously encountered in the builder are now returned as Validation errors.

Twilight 0.15
let response = client.create_message(channel_id)
    .content("I am a message!")?
    .embeds(&embeds)?
    .tts(true)
    .await?;

Twilight 0.16
let response = client.create_message(channel_id)
    .content("I am a message!")
    .embeds(&embeds)
    .tts(true)
    .await?;

Select menu support

Twilight now supports all select menu types. This involves multiple breaking changes to the SelectMenu struct, since not all types of select menus contain all fields. Most notably, the type of the select menu can be checked via the kind field, which is a SelectMenuType.

Support for select menu default values was added via SelectMenu::default_values.

Smaller Discord API catchups

Twilight now supports super reactions via the burst_colors, count_details and me_burst fields on Reaction.

Auto moderation rule creation now supports setting regex patterns and allow list. See CreateAutoModerationRule::with_keyword for the new validation errors returned.

Channel creation and updating now supports specifying a default thread timeout via CreateGuildChannel::default_thread_rate_limit_per_user and UpdateChannel::default_thread_rate_limit_per_user respectively.

The guild onboarding flow can now be modified via the UpgradeGuildOnboarding request.

Creating a stage instance now allows specifying a guild scheduled event via CreateStageInstance::guild_scheduled_event_id.

The current user application can now be edited with the UpdateCurrentUserApplication request and missing fields were added to the Application struct.

The Member::joined_at field is now marked as optional.

The GuildMedia channel type was added.

The unused UserProfile struct was removed from twilight-model, it served no purpose.

Ratelimiter http dependency removal

The HTTP ratelimiter now no longer exposes a dependency on http. Method::to_http was changed to Method::name and now returns a string.

Ecosystem dependency upgrades

The HTTP crate was updated to make use of hyper's latest 1.x version. Gateway, HTTP and Lavalink now use rustls 0.22, up from 0.20. The bitflags crate was updated to 2.x, which changes the methods available on all types generated by it.

Switch to tokio-websockets and fastrand

The lavalink and gateway crates were rewritten internally to switch to the tokio-websockets library, away from tokio-tungstenite. This change should nearly double throughput and efficiency and tightens down on dependency count. We also changed the RNG used by our crates to fastrand.

Deprecated API removal

All APIs deprecated since 0.14.x were removed.

Removal of support for undocumented gateway events

Support for the undocumented GIFT_CODE_UPDATE and PRESENCES_REPLACE events was removed.