Discord bots ahoy!

Hi there!

Hey! Welcome to the first part in a series of indefinite length where I go off about writing a discord bot in python using the discord.py discord library and framework. This series is geared at getting folks who work on the bot I’m responsible for up to speed on what it does and how to work on it. If you’re one of those people, awesome! Hi! And if you found this some other way (maybe from the mastodon auto-post, or from Google or something) well, also hi, but also also don’t be too surprised if there’s context missing. These things happen. You’re clever, though, I’m sure you can figure it out.

If you’re wondering “why are you posting stuff about one specific discord bot for one specific server on your blog”, the answer is it’s either this or a shared google doc and, well… I prefer this way, I have more control over it. (And no, actually doing this on discord would be silly and ridiculous. Discord’s a chat system not a reference system. Seriously, don’t do that)

And if you’re wondering “hey, where’s the actual bot code you’re working with?” the answer there is “eh, too much work to strip it down to its basics” but it’s not hard to go search around and find examples of how to do this. Maybe it’ll be useful to have that as a topic for a later post but for now we’re just assuming either you can see the actual bot source (because you have access to it) or can fake it well enough.

The background info

Here’s the source we’re going to use to talk about the basics of adding a command to our bot and how to manage it. We’re using discord.py’s Cog system, which allows us to dynamically add, remove, and update commands to the bot, and for us this code should live in src/cogs/test.py (the main code for all the cogs lives, for us, in src/cogs). This is about the simplest cog we can build — it doesn’t actually do anything useful but it’s great for experimentation and as example.

from discord.ext import commands

class TestCog(commands.Cog):
    '''
       A cog for testing. Nothing that lives in here is particularly
      interesting.
    '''    

    def __init__(self, bot):
        self.bot = bot
        self.hidden = False

    def unload_cog(self):
        pass

    @commands.command(name="foo”, aliases=[“bar”, “baz”])
    async def foo_cmd(self, ctx: commands.Context):
        '''
           Send the message "you have foo'd!"
        '''
        await ctx.send("you have foo'd!")


async def setup(bot):
    await bot.add_cog(TestCog(bot))

The structure of a cog

A file that contains a cog has four things in it:

  1. A class that derives from discord.ext.commands.Cog
  2. Zero or more commands inside the cog class. Commands have a @command decorator on them
  3. Zero or more event listeners inside the cog class. Listeners have a @listener decorator on them
  4. An async setup function

Our example cog has things , 2, and 4. No event listeners, those are a bit more advanced and we’ll talk about them in another post.

The cog class basics

Our example cog, when loaded, will provide a single command ‘foo’ that will send the string "you have foo’d!" when invoked. There’s no cleverness, no arguments, no fancy processing… nothing. Which is exactly what we want, because it’s an example and clarity is handy here.

The first bit of this we’ll talk about is the class definition. All commands need to live inside a class that inherits from discord.py’s Cog class. We can have other classes inside a file that declares a cog (and we’ll see that a lot later, once we start talking about databases) but that’s optional.

You’ll notice the class has a doctoring attached to it. There are two reasons for this. The first is that it’s just good python practice. Docstrings are more than just comments for future-you as to what this class is for (which is, all by itself, an excellent reason to use them — future-you will always complain about how badly past-you has documented things) though that’s important. For us, docstrings on cogs and command functions are used as part of the help system. If you invoke the help function with no parameters our help command will run through all the cogs in alphabetical order and display their docstrings.

This brings us to the __init__ method. This, as you know, is the method that’s called when instantiating an object for a class. This method is your chance to do any kind of setup necessary for the cog. In our case we do two things. The first is stashing a reference to the bot object we’re passed at creation time (our convention is that all cogs, when instantiated, get passed in that bot object). If we don’t do this we won’t have easy access to this object, and since we stash all the bot state in that bot object that’d be really annoying. So we keep it.

Now, you may be thinking “wait, the bot object must have a handle on all the cogs, so if the cog has a handle on the bot isn’t that a circular handle-on-things loop and aren’t those bad?” and if you are, great! That is indeed the case, there is a circular reference. It’s fine, though — that bot object lives as long as the bot does (so that’s OK), and as we’ll see later there are times when the bot drops its reference to the cog and at that point the cog will get cleaned up and that reference to the bot will go away.

Alternately, if you weren’t thinking about circular references then this is a good example of them and something to think about in the future. Circular references aren’t bad, necessarily, but they are something to keep in mind because it can mean an object we think is dead (and, indeed, may actually be dead) “lives” for longer than it should. Python will clean these up (it takes more than just a circular reference to count as alive!) but it can mean that cleanup gets delayed. We’ll talk about this more when we talk about databases, but that’s something for a future post or three.

The second thing we do in this method is set the hidden attribute to False. This attribute is our general purpose “is this cog for any user? Or is it for admins/mods only?” flag, and any cog with hidden set to True is assumed to contain only commands and event listeners that are… not exactly secret, but not for general server member use. The most obvious result of this is that cogs marked hidden=True won’t show in the help output for regular users. (They still show for members that have mod privileges)

Cog registration

That async def setup(bot) function (well, coroutine) you see at the end of the cog file is one of the things the discord library requires for a cog. When a cog is loaded, the library reads in the .py file with your cog source, checks to make sure there’s a setup function in it, and then calls that setup function, passing in a single parameter, the object representing the bot.

If your file doesn’t have this then the cog won’t be loaded, and the framework will yell at you telling you that it’s missing. So that’s nice. Note that this functions is a package-level function — that is, it’s not a method inside the cog’s class (or any other class) but rather a top-level function in the file. If you mess up the indenting and make it a class method instead then you’ll get yelled at when loading the cog.

This particular setup function is extremely simple, as most of them are. There’s just an await bot.add_cog(TestCog(bot)) inside, which creates an object for our cog (the TestCog(bot) bit) and then registers it as a cog with the bot (the await bot.add_cog() bit). The add_cog method is a co-routine, which is why we await it. (We’ll talk about await and coroutine-ness in a little while)

It’s possible to define multiple cog-derived classes in a single file, and register two or more of them in the setup coroutine. Don’t do that, though, for our bot we go with a one-cog-per-file setup.

Commands in the cog

This is where we (finally!) have some interesting stuff — the actual commands! Woo!

There are a few different ways to register a command with the discord framework, and the way we use is with the @command decorator. (Well, the @commands.command decorator because we don’t import all the way down into the commands namespace but that’s fine, it’s the same thing) Any co-routine (that’s a function defined with async def rather than just def) with that decorator will get loaded into the bot’s command table.

The command-registry decorator has a number of optional named properties. We even set two of them — name and aliases.

name sets the actual name of the command, the thing the bot will respond to. This is generally case-sensitive, and should be lower-case. If it isn’t specified then the command name will be the same as the name of the co-routine that has the @command decorator. But don’t do that, it’s annoying. (We actually do this in a couple of spots and, indeed, is annoying. And I should go fix it)

aliases sets some alternative names for the command. For root commands generally we don’t set aliases (it eats up the space of useful names) but they are useful for command groups. Given we’re not covering groups here that’s not a big deal so you can ignore that for the moment.

The method being decorated must be marked async — this is a requirement of the framework. (For several good reasons) We’ll talk more about what this means later, in another blog post, but you can basically think of it as a method that can run at the same time as other methods.

Every command method takes at least two parameters. The first is self, since this is a method. The second, ctx: commands.Context, is a context parameter. This is something the framework passes into every command handler, and it contains the context for the command. (Which, y’know, you might expect from the fact that it’s type is Context) This includes the message that contains the command, the channel and server the message came from, the user who sent the command, and the guild the command came from, amongst other things.

What’s nice about contexts is they’re Messageables. That means we can use the reply method to reply to them (which will show up to the user who sent the command as the bot replying to the command), and we can use the send method to send a message to the same guild/channel/thread that the command was issued in. That’s what we do here in our little test program — we invoke the send method on the context and send off our little example string. You can try changing the send to reply and see what happens.

The one important thing here to note is that send, and reply, are coroutines. That has a lot of different ramifications that we’ll talk about another time, but the single biggest one is that we can’t just call the method with ctx.send(“some text”), but rather we must use await, and do await ctx.send(“some text”).

Why? The short answer is that when you call a co-routine (that is, a function or method marked async) you don’t actually run the function at that point. Rather python hands you a little magic token that represents the function you want to call. When you await that token (as we do here as part of invoking the send method) that’s when the function is actually run and does its thing.

Python will complain if it goes to destroy one of these coroutine tokens that hasn’t been awaited. If you forget to await a coroutine (or, rather, when you forget, because you will — everyone does at some point) you’ll see a complaint sent the console and in the error log.

What’s with the unload_cog?

You may be wondering what’s up with that unload_cog method that does exactly nothing. Why is it there and what’s the point? It doesn’t do anything, there’s just a pass in there so python doesn’t complain.

You’re right, it doesn’t do anything, and indeed you could absolutely leave it out with no worries. Heck, a lot of our cogs don’t even have one of these functions. This is part of the cog framework we use, though, so it’s here as an illustration.

You’ll remember that we said the cog framework we use allows is for cog unloading and reloading. If a cog has an unload_cog method then that method will get called as part of the unload process. It’ll also get called as part of the reload process — the framework will call unload_cog on the old cog, and once that’s done it’ll instantiate the new cog.

In a lot of cases there’s nothing you really need to do when unloading a cog, so this method is either empty or non-existent. Our bot has a number of registries for a variety of things (overlays for the level card rendering, web fragments for our web interface, and a few of the cogs provide more generic services like audit endpoints) so if a cog added itself to one of those when it was instantiated it needs to remove itself when uninstantiated. (Otherwise we could be in the situation where an old version of a cog is in a registry but a newer version has been loaded for general use and that kind of mess never ends well)

Anyway, in this case there’s really nothing there, just an empty method so we don’t forget. (And for a place to add things later, which we will in future posts)

That’s it for now

And… that’s it. A bare-bones little command for a discord.py based discord bot. Play around with this some and read the library docs — as you can see just from the raw size of them, there’s a lot that you can do with your bot and this example is about as tiny as you can possibly get while still actually doing something.

One thought on “Discord bots ahoy!

Comments are closed.