How to Write a Discord Bot in Rust

Adiákopi Mními on 2020-03-19

Bots are the lifeblood of Discord, so let’s write one of our own

Discord bots are the lifeblood of many communities | Discord Bot by Stereo Maxine

Discord has become a rather ubiquitous alley of communication in a lot of circles. In gaming, for instance, it is a requirement for many games that players have Discord in order to communicate with others. And with that popularity, there’s also a surging desire to create extension programs to enhance the user experience. That’s where bots come into play.

Writing a Discord bot in Rust is mostly about abstraction. Rust is the kind of language where you get to choose the level of abstraction that you’d like for your project while incurring minimal hits to performance. It’s a very useful feature for our purposes here because the Discord application programming interface — or API — tends to be very bare-bones and can make programming in an understandable manner quite difficult.

Fortunately for us, as well there are a few fantastic people who’ve spent a decent chunk of time creating libraries that allow us to make the design choices while not having to worry at all about the inner workings of it beyond how the components interact, which, in my opinion, is standard practice anyway.

One such library is Serenity, a library that abstracts away the things we don’t need to worry about while simultaneously keeping our options available. It wraps around the Discord API and is mostly feature-complete, with voice support and all the works.

Serenity is an all-around fantastic library, but it does have its drawbacks, as with everything. The biggest, and really the only, drawback that this library has is that it leaves multi-threading to the library’s users. This isn’t a deal-breaker for usage; it’s just a minor complaint when it comes to utilizing Rust’s asynchronous features, but for some people, I’d imagine that it’s a pretty big thing to be missing.

Getting Started

As with any project, the first order of business is to actually create the project. So, before we start getting into the code, go ahead and cargo new your project. Name it whatever you’d like as it doesn’t really matter. Mine is called “examplebot” because I didn’t feel like coming up with a better name.

And, as a little side note, I will be hosting this project on GitHub. The code is free to use as you wish, and I will update it for this project as we go along.

The first thing we need to do, now that we’ve created our project, is to add our dependencies, namely serenity . To do that, we’re going to go into our Cargo.toml and, under the [dependencies] entry, add the following:

This adds the Serenity crate to your project as a dependency. And now, we can get to the initialization of a basic bot.

Disclaimer

This article is essentially an endorsement for the Serenity library, although it is not necessarily intentional. Serenity is not the only Discord API wrapper written in Rust, but it is the most developed one I’ve come across.

This article also assumes a certain level of familiarity with the Rust language. If you’re not quite there yet or you’re a beginner looking for an interesting first project, I will be making an article for that specific topic soon. I will update this article with a link to it when I do.

A Ping Bot

This is where a lot of guides tend to begin and end. They show you how to set up a basic bot with the framework and language they’re touting, and they usually don’t go much further than that. We’re going to go a step further and go over the finer points of the Serenity library.

Firstly, we should set up the client.

Now, let’s unpack what we just did.

use statements — The use statement at the top of the file imports all of what we need from the serenity crate. The prelude module contains public facing exports for Serenity, like the Context struct, and model::prelude contains mapped objects that the API dispatches, like Message s.

The Handler — The Handler struct is a unit struct that has no fields and only implements the EventHandler trait from the prelude module. In the message method, the macro unimplemented!() tells the program to panic upon calling the method because it is not implemented. We will remove this when we get to the method in our ping bot.

The main function In our main function, we create a new instance of a Client. The .expect() method call is used here to tell us if and when creation fails and returns a Result.

With client initialization out of the way, let’s create a configuration module. This module will hold data and logic related to application configuration. For the sake of time, let’s just call it config. Now, we create a few constants to hold vital data for our program, like the Discord API token that we need for our bot account or the prefix that we want to use for the commands that we’re going to create.

//in src/config.rs
pub const TOKEN: &'static str = "TOKEN HERE";
pub const PREFIX: &'static str = "PREFIX HERE";

Next, let’s use the power of ron to serialize our configuration into Rusty Object Notation because it’s better than JSON or YAML, in my opinion. To enable use of ron, we have to add the crate to our dependencies as such:

[dependencies]
ron = "0.5.1"

Now that we have it in our dependencies, we’ll declare it in our src/main.rs as an extern crate. Once we’ve done that, we’ll begin by creating a save function in the config module, but before we do, let’s think about what, exactly, we need to do.

Firstly, we need this function to load data from a private module into a Config struct. We need this function to serialize that struct and output the serialized data into a file that we can load with another function when we need to.

With all of that in mind, let’s add the following to our config module:

This function does exactly what we need it to. It serializes our struct and formats it to our liking, and then it writes the data to a file called config.ron; it’s perfect.

Now that we have a save function, we need a load function that deserializes the config file and initializes our app’s settings. This is a fairly simple task that can be accomplished in a few different ways, but we’re going to take the easiest route. We’re going to utilize the std::fs::File struct in a similar fashion to what we did with the save function.

Start underneath the save function and add a new pub fn called load.

Let’s see exactly what’s happening here. Firstly, we imported the de module from the ron crate. This allows for our future call to de::from_reader. Then we declared a pub fn of type std::io::Result<Config>. Why are we returning a result, when we can just return the Config instance directly, you may ask? Because of errors.

The Result enum allows for concise error handling. By returning the Ok(config), we tell the compiler that everything went as planned. Now, if we had returned Err(config) instead, that would tell the compiler that something went wrong. This is called a soft panic. It’s not fatal for your program; it’s a recoverable error, as the Rust book refers to them. Recoverable errors can be handled in a match or if let statement, and that’s what we’ll do later in the project.

Now that we have our config module set up, let’s take a look at what it should look like in its entirety:

Now that we’ve reviewed our work in the config module, let’s look at the root module, src/main.rs again.

We can now utilize our Config struct declared in the config module to pass data to the client initialization. To do that, we have to first create a new instance of the struct ( Config::new() ) and then, if the file doesn’t already exist, save the configuration. If the file already exists, then we’re set to call Config::load().

Edit the main function and add a variable to hold the Config instance. At the top of the function, add let config = Config::new();. This creates a fresh instance of the Config struct, and so we can save the Config to a file or, if the file exists, we can load it. This is probably where you’d have some sort of match or if let statement to do one or the other, depending on whether the file exists or not. That is optional, but for our purposes, we’re going to omit it.

We now have our Config up and running. And since we do, we’re going to implement the ping functionality for our bot using the command framework that serenity provides.

Implementing the Pings and Pongs

We can have a basic ping program just by implementing the message method from the EventHandler trait.

impl EventHandler for Handler {
  fn message(&self, context: Context, msg: Message) {
    if msg.content == "!ping" {
      if let Err(why) = msg.channel_id.say(&context.http, "Pong!") {
        println!("Error sending message: {}", why);
      }
    }
  }
}

That snippet would make it so that a message containing !ping triggers a message from the bot that says “Pong!” Easy enough, but it’s not what we’re going for in this project.

The thing about handling commands through the EventHandler trait is that it isn’t particularly scalable. Imagine having hundreds of commands that are all being checked through a massive match statement. That would be hell for the poor developer tasked with maintaining it, let alone for the person who wrote it in the first place.

So what can we do, if we don’t want to handle our commands manually? Well, serenity happens to offer a nice command framework. The library has an exported procedural macro (attribute) that brings in a lot of utility without us really having to do anything besides write the code for the commands.

Commands

Creating commands mostly just involves working with the Context type. Not much effort is involved as it really comes down to how you want your command to appear to the end user.

When writing our first command, there are a couple of things that we need to consider: in what context will this command be used, and how we want the user to be able to interact with it.

For instance, with our ping command, we don’t really need the user to interact with it, and we don’t really have to worry about the context in which it’s called. All we need do is reply with a set message when a user sends a message that matches our criteria.

Take this snippet of code:

This function takes two parameters: a mutable reference of type Context and a Message. It returns a CommandResult. The two parameters are filled in automatically, and we never have to worry about calling the function manually as it is taken care of by the library.

The ping command we have is the same as the one we wrote before in the EventHandler, but it has a few key differences. The main one is that it’s declared in its own function, which makes it easier to maintain. And then, there’s the use of CommandResult, which serenity uses to handle errors internally.

This command is also set to be a part of a group, which we can set with the group attribute on a unit-like struct with no fields.

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

This creates a group called Public that contains the ping command.

Now, in src/main.rs, we’re going to add a framework configuration.

client.with_framework(StandardFramework::new()
    .configure(|c |
      c.prefix(config.prefix()))
    .group(&PUBLIC_GROUP));

This adds the standard serenity framework to the client instance, and it configures the prefix and adds command groups to the instance as well.

With all of these snippets in mind, our project should look like this.

You now have a functional Discord bot, written in Rust, that responds to a ping command. This is all well and good, but there is one more thing that I’d like to touch on before the end of this story: embeds.

Embeds in Serenity

Embeds in serenity are rather easy to create. You can utilize the send_message method and create an embed as a parameter.

This sends a message to the channel from which the command was received. In the message, an embed is created and applied to that message. It’s a quick and easy way to build an embed the way you want to, and there are more fields you can create, like the footer . In our example, we could add a footer to the embed just by adding a e.footer() to the embed method parameter.

You can utilize this in a function. Try adding this to your src/commands.rs:

Now that you have this knowledge that you (totally) didn’t have before, go out and make something wonderful for your Discord server, or, if you want, for a group DM with your friends and see what fun you can have with it!

Resources

Serenity: GitHub source code repository for the Serenity library

Documentation: Source code documentation for the Serenity library

Examplebot: My example Discord bot written in Rust using Serenity

Thank you for reading, and have a wonderful day!