Rust, Serenity, and Discord oh my! (Part I)


A neverending story of development and diminishment

Although I do not have much experience with Rust, it is a very appealing programming language to me. To deepen my understanding and in general get more familiar with it, I decided to start a small little project - coding a bot for the popular platform Discord.

After some short research, I chose to use the serenity library for my bot. You can find it here.

Of course, I couldn’t just dive into the documentation and create the bot from scratch - my general knowledge of Rust and Discord are way too lacking for this. Luckily, serenity provides a few very detailed examples. While I could’ve probably written my entire bot by basically copy-pasting their examples, I decided to follow a nifty little tutorial online. It’s pretty short, and you can work through it in very little time: Click here!

Honestly, I was surprised that of all the people that could’ve made that tutorial, it was our favourite totally-not-a-lizard-robot’s company - it was of pretty high quality, but had less than 1k views.

At the end of that tutorial, I ended up with the following code:

const HELP_MESSAGE: &str = "
Hello there, Human!
You have summoned me. Let's see about getting you what you need.
— 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);
    }
}

Well, that wasn’t too bad now was it? The tutorial was also really helpful with actually getting this code to interact with Discord, so I could get to testing it rather quickly.

Testing the waters

So I booted up the bot, added it to my own little server, wrote !help, and to my surprise, it worked. Hurray!

So…what now?

Even the not-so observant reader should already see at least one large issue with the above code: if msg.content == HELP_COMMAND {...}

This code is about as unscalable as possible. While I can change the command rather quickly if I so desire, adding more commands would be incredibly tedious. Having the bot go through a gigantic match everytime someone writes a message (not just a command!), not being able to edit the prefixes for all commands (which is important for servers where many prefixes might already be occupied) and a few other quirks call for drastic measures.

Back to square 1

With (fairly limited) experience under my belt, I decided to dive back into the depths of the examples in serenity. For those of you out there inclined to follow along with me, the most helpful example has been the command framework, which you can find here.

First of all - the wall of code might seem daunting at first, but it can be broken down into small, digestible bits. Luckily, the folks over at serenity’s github page documented the examples really well - there’s almost no line which doesn’t have an explanation and/or context.

I won’t go into detail here, as the example probably explains it better than I ever could. I will however share my first “own” command I implemented: A simple roll command, that picks a random number between 1 and the argument passed:

#[command]
async fn roll(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
        let sides : u32 = args.single::<u32>().unwrap();
        let result = rand::thread_rng().gen_range(1,sides+1);
        let say_content = format!("You rolled a `{}`!", result);

    msg.channel_id.say(&ctx.http, say_content).await?;

    Ok(())
}

Hurray! The bot actually provides a useful functionality now! However, it is clearly very limited and prone to errors. In the next post, I will delve deeper into what improvements I’ve made to the command since, and the general setup of the bot.