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.
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.
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:
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.
In order to start writing this Rust application, there are a few requirements:
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.
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.
I already have an account, but if you don’t, you can sign up for free.
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
This project requires two external dependencies:
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.
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);
}
}
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.
In order to test this out, I’ll need my own Discord Server. To create one, follow these steps:
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.
I’ll also need to add a role. To do this, follow these steps:
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.
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.
The next step is to create a Discord Bot. Think of this as the profile for the bot. To create one, do the following:
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:
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!
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.
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 “” 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.
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.
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!
If you’d like to continue hacking on this project further, here are a few ideas:
Let me know if you do this or something similar. We’d love to see what you build.
#rust #programming #developer