返回開發人員最新消息

Build a Discord Bot with Rust and Serenity

2020年9月30日

By Joe Previte

Discord is on the rise in developer communities. And, as we all know, developers love building on top of platforms they use. It’s fun!

Today, I’m going to show you how to build your own Discord bot using Rust and serenity.

Building the App

Before building the app, I’ll cover how it will work and the prerequisites for following along. After that, I’ll jump in and go through each step before setting it up to run locally on your machine. Finally, I’ll show how to test it out in your own Discord server.

How will it work?

Imagine I’m creating a bot for a developer community Discord server. I’m going to build a Discord bot which supports a single command !help, which will return a message explaining:

  • What channel to post in for technical help
  • Where to find the code of conduct
  • How to get in touch with admins of the server

This could be helpful for new people who need help for various scenarios. Think of it being analogous to the --help flag commonly used by CLIs.

Prerequisites

In order to start writing this Rust application, there are a few requirements:

  • Rust installed locally
  • IDE setup for Rust development
  • Discord account

Install Rust

I already have Rust installed locally. If you don’t, you can install it locally using:

            shell
            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
          

Afterwards, run rustup --version to verify that it worked. If it did, you should see something printed to your terminal.

IDE for Rust Development

I’m going to be using VS Code and the rust-analyzer extension. You can find support for other IDEs under the Tools on the rust-lang website.

Discord Account

I already have an account, but if you don’t, you can sign up for free.

Set up Code

Since this is a new project, I’m going to create a new project with cargo. For simplicity, I’ll name the project discord-help-bot.

          shell
          cargo new discord-help-bot
        

Add Dependencies

This project requires two external dependencies:

  • tokio - “A runtime for writing reliable, asynchronous and slim applications with the Rust programming language”
  • serenity - “a Rust library for the Discord API”

I’ll add both to the Cargo.toml file:

          toml
          [dependencies]
          tokio = { version = "0.2", features = ["macros"] }
          serenity = { default-features = false, features = ["client", "gateway", "model", 
          "rustls_backend"], version = "0.9.0-rc.2"}
        

Note: you’ll notice this is a release candidate (rc). When you go through this, check to make sure you’re using the latest version of serenity.

tokio allows the program to run asynchronously and serenity allows you to interact with the Discord API. With both of these dependencies, I can start adding logic to the program.

Add Logic to main.rs

Since this is a small program, I will only need to add code to the main.rs file. I’ll add the code and then afterwards I’ll walk through how it works.

          rust
          use std::env;

          use serenity::{
          	async_trait,
          	model::{channel::Message, gateway::Ready},
          	prelude::*,
          };

          const HELP_MESSAGE: &str = "
          Hello there, Human!

          You have summoned me. Let's see about getting you what you need.

          ? Need technical help?
          => Post in the <#CHANNEL_ID> channel and other humans will assist you.
          
          ? Looking for the Code of Conduct?
          => Here it is: <https://opensource.facebook.com/code-of-conduct> 
          
          ? Something wrong?
          => You can flag an admin with @admin
          
          I hope that resolves your issue!
          -- Helpbot
          
          ";

          const HELP_COMMAND: &str = "!help";

          struct Handler;

          #[async_trait]
          impl EventHandler for Handler {
            async fn message(&self, ctx: Context, msg: Message) {
              if msg.content == HELP_COMMAND {
                if let Err(why) = msg.channel_id.say(&ctx.http, HELP_MESSAGE).await {
                	println!("Error sending message: {:?}", why);
              }
            }
          }

          async fn ready(&self, _: Context, ready: Ready) {
          	println!("{} is connected!", ready.user.name);
          	}
          }
          #[tokio::main]
          async fn main() {
            let token = env::var("DISCORD_TOKEN")
            .expect("Expected a token in the environment");

            let mut client = Client::new(&token)
            .event_handler(Handler)
            .await
            .expect("Err creating client");

            if let Err(why) = client.start().await {
            	println!("Client error: {:?}", why);
            }
          }
        

Code Walkthrough

I’m going to walk through the code in four chunks. In the first chunk, I’ll look at the following:

          rust
          use std::env;

          use serenity::{
          	async_trait,
          	model::{channel::Message, gateway::Ready},
          	prelude::*,
          };
        

These are the use declarations. They make it easier for developers because they “shorten the path required to refer to a module item.” Here, there are two blocks. The first refers to the env module from the standard library, which we later use to access the DISCORD_TOKEN environment variable.

The next block refers to the modules I use provided by serenity. The first is async_trait which I use on the Handler to tell the compiler the type and methods Handler should support. After that are two structs, Message and Ready. The first is used inside the function signature of message to indicate the type for the third parameter msg. As you can guess, this is for the shape of the message when we receive it from the Discord server. The other struct is Ready and is used in the function signature of the ready function for our Handler. The last line here is the prelude which says, “include the basic things out of the box for the user.”

In the second chunk, I’ll discuss the following piece of code:

          rust
          const HELP_MESSAGE: &str = "
          Hello there, Human!

          You have summoned me. Let's see about getting you what you need.
          
 					? Need technical help?
          => Post in the <#CHANNEL_ID> channel and other humans will assist you.
          
          ? Looking for the Code of Conduct?
          => Here it is: <https://opensource.facebook.com/code-of-conduct> 
          
          ? Something wrong?
          => You can flag an admin with @admin
          
          I hope that resolves your issue!
          -- Helpbot
          
          ";

          const HELP_COMMAND: &str = "!help";
        

I declare both HELP_MESSAGE and HELP_COMMAND using the const keyword because they stay constant throughout the lifetime of the program and don’t change. With const, you must explicitly annotate the type. We use the &str because these are string slices.

In the third chunk, I’ll look at the following:

          rust
          struct Handler;

          #[async_trait]
          impl EventHandler for Handler {
            async fn message(&self, ctx: Context, msg: Message) {
              if msg.content == HELP_COMMAND {
                if let Err(why) = msg.channel_id.say(&ctx.http, HELP_MESSAGE).await {
                	println!("Error sending message: {:?}", why);
              }
            }
          }

          async fn ready(&self, _: Context, ready: Ready) {
          	println!("{} is connected!", ready.user.name);
          	}
          }
        

In this part of the program, I declare struct Handler. This doesn’t do much because all it does is declare the struct without any fields. In the next block, we use the #[async_trait] macro to tell the compiler that the struct below implements that trait like allowing us to use the async keyword with our functions and the .await method.

After that, the impl EventHandler for Handler tells the compiler, “My struct called Handler is going to look like an EventHandler.” Inside the struct are two functions: message and ready. The message function is where the main logic of our program happens. It takes in a message, checks the content to see if it matches the HELP_COMMAND and it does, it sends the HELP_MESSAGE to that channel using the same channel id. If there’s an error, it prints it.

The ready function logs a statement letting us know the handler for our Discord bot is ready using the bot’s name.

Last, the final chunk I have to walk through is:

          rust
          #[tokio::main]
          async fn main() {
            let token = env::var("DISCORD_TOKEN")
            .expect("Expected a token in the environment");

            let mut client = Client::new(&token)
            .event_handler(Handler)
            .await
            .expect("Err creating client");

            if let Err(why) = client.start().await {
            	println!("Client error: {:?}", why);
            }
          }
        

The first thing I see is the #[tokio::main] macro which is used because this is an asynchronous application. The next is the main function which is called when the program runs. The first thing it does is get the DISCORD_TOKEN environment variable. Then, it creates a new Serenity Client using the token for us to talk to the Discord API. Last, the program starts the client and handles the error if it has issues starting up.

And that’s all the logic for the program! Onto the next step.

Create a Discord Server

In order to test this out, I’ll need my own Discord Server. To create one, follow these steps:

  • Open the Discord application
  • On the left side, click the plus icon “Add a Server”
  • Click “Create a server”
  • Once you’ve filled everything out, click “Create”

Create Channel and Get Channel ID

Since I’m starting from scratch, I’ll need to create a new channel. I can do this by clicking the plus icon on the left next to “TEXT CHANNELS”. I’ll call mine “help”.

I need to get the channel ID. Discord makes it easy to expose this in the UI if you toggle on Developer Mode. To get here, go to Preferences > Appearance > Developer Mode.

To see the channel ID, right click on the channel and select “copy ID”.

Returning to the application, replace the “CHANNEL_ID” placeholder text with yours. After words, it should look something like `<#750828544917110936>`. Make sure you have the angled brackets and the “#” symbol.

Add a Role

I’ll also need to add a role. To do this, follow these steps:

  • In the top left, click on the server dropdown menu
  • Select “Server Settings”
  • Go to Roles on the left
  • Click the plus next to Roles in the middle
  • Type in the role name - I’m using the name “admin”
  • Click “Save changes”

To make yourself that role, right click on your Discord handle either in a message on the right sidebar, select Roles > admin.

Now that I have the role set up, someone can actually use the @admin to get my attention.

Create Discord Application

In order to create a Discord bot, I first need to create a Discord application. Follow these steps to do this:

After this is done, I can move on to the next step to create a bot.

Create Discord Bot and Install on Discord Server

The next step is to create a Discord Bot. Think of this as the profile for the bot. To create one, do the following:

  • On the left sidebar, click “Bot”
  • Click “Add Bot”
  • Feel free to change the name and the icon here
  • After you’re done, click “OAuth2” on the left sidebar
  • Under scopes, select “bot”
  • Scroll down to permissions and select “Send Messages” and “Read Message History”
  • Scroll up and click “copy” to copy the generated OAuth url
  • Paste it in a new tab in your browser
  • It will ask you which server you want to add it to. Note: you can only add bots to servers where you have the “Manage Server” permissions. Since we created our own, we do. Add it to the server you created earlier.
  • Click “Continue”
  • Confirm that you want to allow the bot to send messages and click “Authorize”

Great! Now the Discord bot is created.

Before I leave the Discord Developer Portal, I need one last thing: the Discord Bot auth token. To see it, follow these steps:

  • Click “Bot” on the left sidebar again
  • Next to the bot icon, look for the token
  • Click “copy”

I’m going to save this now because I’ll need it later when I run the application locally. Also, a friendly reminder to not commit this to git. You don’t want your auth token to get in the wrong hands!

Build Code

I am ready to build my project for testing locally. To this, I can run:

          shell
          cargo build 
        

If the compiler is happy with my code, it will output my code under /target/debug/ and contain an executable using the name key in the Cargo.toml. In my case, this is discord-help-bot.

Run Locally

It’s time to run the executable locally and see if my bot starts up as expected. I will run the command:

          shell
          DISCORD_TOKEN=<use your token here> ./target/debug/discord-help-bot
        

I’ll replace “<use your token here>” with my token from earlier. This makes the token available as an environment variable to the application.

If it worked, you should see a message printed to the terminal saying “BotName is connected!” where “BotName” shows the name of your bot according to what you named it.

Test Bot on Discord Server

The moment I’ve been waiting for - testing my bot on my Discord server. With the bot still running from the previous step, I will open my Discord server where the bot is installed and send the message “!help” in the #general channel. And it works! I see the help message I wrote earlier posted to the channel almost immediately by my bot.

Woohoo! Mission accomplished.

Summary

Congratulations on making it through this project with me! I had a lot of fun and hope you did as well. I built a basic Discord bot with Rust and tested it on my Discord server by running it locally.

If you’d like to see a full-working version of this, you can check it out here on GitHub. If you want to make this better, I encourage you to open a pull request! You can ask questions there in an issue or open an issue if you find a bug. All contributions are welcome!

Next Steps

If you’d like to continue hacking on this project further, here are a few ideas:

  • Print a message to the console along with the username of the person who interacted with the bot
  • Add an avatar/icon to the bot
  • Add a second command to the bot
  • Deploy the bot

Let me know if you do this or something similar. We’d love to see what you build.

Resources for Learning More Rust

Looking for more ways to develop your Rust skills? Here are a few recommendations for continuing your learning:

Thanks for reading. Happy coding!

Special thanks to the maintainers of Serenity for the project examples and Will Harris’ article “How to Add a Bot to Discord” both of which made writing this tutorial possible.

To learn more about Facebook Open Source, visit our open source site, subscribe on Youtube, or follow us on Twitter and Facebook.

Interested in working with open source at Facebook? Check out our open source-related openings on our career page by taking this quick survey.