How to Write a Discord Bot in Rust

Discord is an in­stant mes­sage plat­form with more than 150 mil­lion monthly ac­tive users. The main ap­peal seems to a com­bi­na­tion of the wide reach that plat­forms like StackOverflow have, as well as the in­stant de­liv­ery of the in­stant mes­sag­ing con­cept. One of the best fea­tures of Discord is it’s seem­ingly un­lim­ited hack­a­bil­ity of the plat­form via it’s Bot” sys­tem. The Bot sys­tem al­lows de­vel­op­ers to add func­tion­al­ity to Discord com­mu­ni­ties by writ­ing soft­ware that in­ter­faces in a sim­i­lar way that peo­ple do. I want to give you an in­tro­duc­tion on how to do that in Rust.

A Flat Render of Rust's Mascot, Ferris

Interaction

Most in­ter­ac­tion with Discord bots hap­pens via com­mands, not dis­sim­i­lar to ter­mi­nal ap­pli­ca­tions. Commands may look like !play Eat it by Weird Al.

Ping! Pong!

The bot we are go­ing to make now will sim­ply re­spond to !ping with Pong!”.

Template

cargo generator template

Setup the Project

Using a func­tion­ing Rust en­vi­ron­ment, use cargo to cre­ate a new pro­ject. E.g:

cargo new tutorial-bot

Next, we have to add Serenity, the li­brary for cre­at­ing Discord bots in Rust. We also have to drop in Tokio, be­cause Serenity takes ad­van­tage of it’s async run­time.

You can do this ei­ther via cargo-edit:

cargo add serenity
cargo add tokio --features full

or by just adding them to Cargo.toml:

[dependencies]
serenity = "0.10.5"
tokio = { version = "1.5.0", features = ["full"] }

Setting up the Standard Framework

Serenity has a lot of flex­i­bil­ity. You have ac­cess to a event han­dler that al­lows fine grain con­trol of events. You also have ac­cess to a stan­dard frame­work that makes it ridicu­lously easy to re­spond to com­mands. Be­fore we do any­thing else, we have to make our main func­tion async. It is su­per easy to do that, just re­place it with:

#[tokio::main]
async fn main() {
}

First, we want to get our bot to­ken in. In an ac­tual bot, please ob­tain it via an en­vi­ron­ment vari­able or some other method. We are only do­ing it this way for sim­plic­ity.

let token = "{your bot token}";

If you do not know how to get a bot to­ken, please fol­low this tu­to­r­ial.

The Serenity Standard Framework splits your bot’s com­mands into groups. Each group can have mul­ti­ple com­mands. For ex­am­ple, a bot might have two groups: one fo­cused on fun and one fo­cused on math. The for­mer has com­mands like !meme, while the lat­ter may have var­i­ous math func­tions, like sin!. This is also how we will add com­mands to our bot.

First, add the needed structs and macros to the file:

use serenity::{Client, client::Context, framework::{StandardFramework, standard::{CommandResult, macros::{group, command}}}, model::channel::Message};

Next, cre­ate a struct that we will at­tach our com­mands to:

#[group]
#[commands()]
struct HelloWorld;

Once we have added our com­mands, we will en­ter them into the com­mands sub-macro.

Create an in­stance of StandardFramework and add our group to it. We can also con­fig­ure our com­mand pre­fix now.

let framework = StandardFramework::new()
    .configure(|c|{
        c.prefix("!")
    })
    .group(&HELLOWORLD_GROUP);

Notice that we used a ref­er­ence to a sta­tic struct called HELLOWORLD_GROUP in­stead of just adding our group. This is the out­put of the #[group] macro.

Now that we have cre­ated our frame­work, we have to at­tach it to a Discord client.

let mut client = Client::builder(token).framework(framework).await.expect("Could not start Discord");

Start it.

client.start().await.expect("The bot stopped");

Adding the Command

Now that we have the frame­work set up, let’s add a com­mand.

#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult{
}

This com­mand only needs the Discord clien­t’s Context, the mes­sage that con­tains the com­mand, and re­turns a CommandResult.  Do not for­get to add the com­mand to the group:

#[group]
#[commands(ping)]
struct HelloWorld;

If you don’t, it will sim­ply not get run.

We want the bot to re­ply to the !ping com­mand with Pong!”, so let’s add that to the in­side of the ping func­tion:

msg.reply(ctx, "Pong!").await?;
Ok(())

Full Code

Here is the full code for the bot:

use serenity::{Client, client::Context, framework::{StandardFramework, standard::{CommandResult, macros::{group, command}}}, model::channel::Message};
#[tokio::main]
async fn main() {
    let token = "Your bot token";
    let framework = StandardFramework::new()
    .configure(|c|{
        c.prefix("!")
    })
    .group(&HELLOWORLD_GROUP);
    let mut client = Client::builder(token).framework(framework).await.expect("Could not start Discord");
    client.start().await.expect("The bot stopped");
}
#[group]
#[commands(ping)]
struct HelloWorld;
#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult{
    msg.reply(ctx, "Pong!").await?;
    Ok(())
}

That’s it

If you build and run your app, you should have a func­tion­ing Discord bot! Seren­ity is an amaz­ing crate and is an ab­solute joy to work with. I hope you learned some­thing. There is a ton more stuff that I did not cover here. Feel free to look at the Serenity docs and ex­am­ples to learn more!