Overview
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.
Links
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();
}
Links
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(())
}
Links
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(())
}
Links
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(())
}
Links
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(())
}
Links
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());
}
Links
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(())
}
Links
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:
CommandBuilder
EmbedBuilder
- [
InteractionResponseData
]
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(())
}
Link
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();
}
Links
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.
Links
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; andcreate_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:
ShardEventStream
to poll a group of shards for the next gateway event; andShardMessageStream
to poll a group of shards for the next WebSocket message.
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
CommandOptionChoice
s 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_timeout
s 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 CommandOption
s 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:
MessageType::StageEnd
, denoting when a stage ends;MessageType::StageSpeaker
, denoting when a member becomes a speaker;MessageType::StageStart
, denoting when a stage starts; andMessageType::StageTopic
, denoting when the topic of the stage changes.
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.