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


A neverending story of rolling and rerolling

Most of this post is self-contained, but for full context, check out part 1.

In this part of my little series about the various projects I code for my own little discord bot, I’ll tell you more about the very first, simple command: A simple roll command, which essentially serves as a random number generator. Many chatplatforms have such a command integrated - but not Discord. Let’s fix this, shall we?

Where we left off

I ended part one with the following code snippet:

#[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(())
}

As alluded to, this code provides the very basic functionality that I had aimed for, but is very errorprone.

Luckily, with this being a Discord bot, I don’t have to worry about testing the features extensively myself - I just announce the new feature on the CS Discord Server, and wait for the horde of bored (and probably procrastinating) CS students to try and break the command in any way possible.

With this simple command, there is of course a lot that can go wrong, and I don’t even need to test to see that. Let’s collect some issues:

Issues with roll

  • Number of arguments could be zero or more than one
  • Argument given could be unparsable as u32 (think someString or similar)
  • No help/usage info
  • u32 doesn’t allow for very large numbers

Of course, there are probably way more - but those are the ones that I decided on fixing.

How precise is enough?

When I said that u32 doesn’t allow for very large numbers, the naive solution would be to use u64. While allowing for much, much larger numbers, this was not enough for me. What do I mean by that? Consider the maximum value that an unsigned, 64-bit integer can take: 2^64-1 = 18'446'744'073'709'551'615. For any sane person, this will probably be far beyond any reasonable number they decide to roll for - but my target demographic hardly qualifies as ‘sane’.

A little excursion: Discord allows for up to 2000 characters per message. Subtracting the 6 characters that a potential user needs to invoke the command, this leaves 1994 characters for their desired maximum possible outcome. Using u64, the bot would only be able to handle a mere 19 digits, 20 if they’re on the low side. Can we do better?

We’re going to need a larger representation

Of course! But not with the stuff that’s built into std. I chose to use a crate where the name says it all: num-bigint. What does bigint do? Well, it allows for arbitrarily large numbers to be stored and used.1 Additionally, it supports random generation of such large numbers with reasonable (read: really fast) speeds.

The small bot program changes only very, very slightly: We mostly switch every occurence of u32 with BigInt, and for the random generation, we use rand::thread_rng().gen_bigint_range() with the user-given range.

Signed: Azurios

We aren’t done yet though! There’s still the possibility of nonsensical results being input by the user. Besides using completely nonsensical input (e.g. inputting a string or char), our current implementation technically allows for signed integers to be input without failing the parse. While it is tempting to simply ‘do nothing’ or give an error to the user, I thought it fun to have the bot respond to certain inputs, namely for inputting 0 or a negative number. This is easily done:

        let sides: BigInt = args.single::<BigInt>()?;
        let say_content = if sides == BigInt::from(0) {
            String::from("How many edges does a zero-sided die have?")
        } else if sides < BigInt::from(0) {
            format!("I tried rolling a D-`{}`, but I failed :(", sides)
        } else {
            let result: BigInt =
                rand::thread_rng().gen_bigint_range(&BigInt::from(1), &(sides + BigInt::from(1)));
            format!("You rolled a `{}`!", result)
};

You input WHAT?

While we’re at it, let’s handle the error we catch in the first line via ? a tad bit more gracefully:

       let sides: BigInt = match args.single::<BigInt>() {
           Ok(res) => res,
           Err(e) => {
               msg.channel_id
                   .say(ctx, "I don't think that's going to work.")
                   .await?;
               return Err(From::from(e));
           }
       };

In the first few iterations of this command, I had handled the inputting of none or too many arguments manually, that is, I checked for number of arguments and gave output depending on that. But there is no need for that: Serenity provides with its command macros a nifty little thing called #[num_args], which defines the number of arguments expected for a command. If any other amount is input, the command propagates an error upwards, where we can further customize the response with a handler for so called dispatch errors, which is shared for all commands! The following snippet illustrates how we’ll deal with those errors:

async fn dispatcherr(ctx: &Context, msg: &Message, err: DispatchError) {
    match err {
        DispatchError::NotEnoughArguments { min, given } => {
            msg.channel_id.say(&ctx, format!("Sorry, but I can't handle the command this way. 
	    You gave me {} arguments, while I need at least {}. Maybe try again?", given, min))
	    .await.unwrap();
            ()
        }
// this goes on for every error type
// ...
//

Phew, that’s a lot of errors handled and a good start for our little bot! I can’t stress enough how great serenity’s built-in command framework is, as it allows easily reusing code that should be shared across commands.

So, what’s left to do? Well, of course, our bot should have more features than just a random number generator. So next time, we’ll focus on another command entirely, which will do some more complex things.

If you want to check out the bot in its current stage, you can do so here.

While you’re here

I’m officially part of the one, the only, the legendary Polyring. If you’ve read this far, you’ll likely find the blogs of the other members very interesting, so do check them out below!


  1. There is some technical limitation which gives a rough upper bound of 3.079e+2221209315409342851. Read more about how to get this number here. As elementary mathematics tell us, 1994 < 3.079e+2221209315409342851, so it suffices for our needs. ↩︎