{"id":67,"date":"2024-06-09T21:34:24","date_gmt":"2024-06-10T01:34:24","guid":{"rendered":"https:\/\/www.sidhe.org\/blog\/?p=67"},"modified":"2024-06-09T21:34:24","modified_gmt":"2024-06-10T01:34:24","slug":"discordbotbasics","status":"publish","type":"post","link":"https:\/\/www.sidhe.org\/blog\/2024\/06\/09\/discordbotbasics\/","title":{"rendered":"Discord bots ahoy!"},"content":{"rendered":"<div class=\"boldgrid-section\">\n<div class=\"container\">\n<div class=\"row\">\n<div class=\"col-lg-12 col-md-12 col-xs-12 col-sm-12\">\n<h2 class=\"\"><b>Hi there!<\/b><\/h2>\n<p class=\"\">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\u2019m responsible for up to speed on what it does and how to work on it. If you\u2019re 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\u2019t be too surprised if there\u2019s context missing. These things happen. You\u2019re clever, though, I\u2019m sure you can figure it out.<\/p>\n<p class=\"\">If you\u2019re wondering \u201cwhy are you posting stuff about one specific discord bot for one specific server on your blog\u201d, the answer is it\u2019s either this or a shared google doc and, well\u2026 I prefer this way, I have more control over it. (And no, actually doing this <i>on discord<\/i> would be silly and ridiculous. Discord\u2019s a chat system <i>not<\/i> a reference system. Seriously, don\u2019t do that)<\/p>\n<p class=\"\">And if you\u2019re wondering \u201chey, where\u2019s the actual bot code you\u2019re working with?\u201d the answer <i>there<\/i> is \u201ceh, too much work to strip it down to its basics\u201d but it\u2019s not hard to go search around and find examples of how to do this. Maybe it\u2019ll be useful to have that as a topic for a later post but for now we\u2019re just assuming either you can see the actual bot source (because you have access to it) or can fake it well enough.<\/p>\n<p class=\"\"><!--more--><\/p>\n<h2 class=\"\"><b>The background info<\/b><\/h2>\n<p class=\"\">Here\u2019s the source we\u2019re going to use to talk about the basics of adding a command to our bot and how to manage it. We\u2019re using <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/index.html\">discord.py<\/a>\u2019s <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/cogs.html\">Cog<\/a> system, which allows us to dynamically add, remove, and update commands to the bot, and for us this code should live in <code>src\/cogs\/test.py<\/code> (the main code for all the cogs lives, for us, in <code>src\/cogs<\/code>). This is about the simplest <i>cog<\/i> we can build \u2014 it doesn\u2019t actually do anything useful but it\u2019s great for experimentation and as example.<\/p>\n<pre class=\"\" style=\"border-style: solid; border-width: 4px;\">from discord.ext import commands\r\n\r\nclass TestCog(commands.Cog):\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; <\/span>'''\r\n<span class=\"Apple-converted-space\">&nbsp;&nbsp; &nbsp; &nbsp; <\/span>A cog for testing. Nothing that lives in here is particularly\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; &nbsp; <\/span>interesting.\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; <\/span>''' <span class=\"Apple-converted-space\">&nbsp; &nbsp;<\/span>\r\n\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; <\/span>def __init__(self, bot):\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; &nbsp; &nbsp; <\/span>self.bot = bot\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; &nbsp; &nbsp; <\/span>self.hidden = False\r\n\r\n    def unload_cog(self):\r\n<span class=\"Apple-converted-space\">  &nbsp;     <\/span>pass\r\n\r\n<span class=\"Apple-converted-space\">&nbsp; &nbsp; <\/span>@commands.command(name=\"foo\u201d, aliases=[\u201cbar\u201d, \u201cbaz\u201d])\r\n<span class=\"Apple-converted-space\">  &nbsp; <\/span>async def foo_cmd(self, ctx: commands.Context):\r\n        '''\r\n           Send the message \"you have foo'd!\"\r\n        '''\r\n<span class=\"Apple-converted-space\">  &nbsp; &nbsp; &nbsp; <\/span>await ctx.send(\"you have foo'd!\")\r\n\r\n\r\nasync def setup(bot):\r\n<span class=\"Apple-converted-space\">  &nbsp; <\/span>await bot.add_cog(TestCog(bot))\r\n\r\n<\/pre>\n<h2 class=\"\">The structure of a cog<\/h2>\n<p class=\"\">A file that contains a <i>cog<\/i> has four things in it:<\/p>\n<ol class=\"\">\n<li>A class that derives from discord.ext.commands.Cog<\/li>\n<li>Zero or more commands inside the cog class. Commands have a @command decorator on them<\/li>\n<li>Zero or more event listeners inside the cog class. Listeners have a @listener decorator on them<\/li>\n<li>An async setup function<\/li>\n<\/ol>\n<p>Our example cog has things <a rel=\"tag\" class=\"hashtag u-tag u-category\" href=\"https:\/\/www.sidhe.org\/blog\/tag\/1\/\">#1<\/a>, 2, and 4. No event listeners, those are a bit more advanced and we\u2019ll talk about them in another post.<\/p>\n<h2><b>The cog class basics<\/b><\/h2>\n<p class=\"\">Our example cog, when loaded, will provide a single command \u2018foo\u2019 that will send the string <code>\"you have foo\u2019d!\"<\/code> when invoked. There\u2019s no cleverness, no arguments, no fancy processing\u2026 nothing. Which is exactly what we want, because it\u2019s an example and clarity is handy here.<\/p>\n<p>The first bit of this we\u2019ll talk about is the class definition. All commands need to live inside a class that inherits from discord.py\u2019s Cog class. We can have other classes inside a file that declares a cog (and we\u2019ll see that a lot later, once we start talking about databases) but that\u2019s optional.<\/p>\n<p class=\"\">You\u2019ll notice the class has a doctoring attached to it. There are two reasons for this. The first is that it\u2019s 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 \u2014 future-you will <i>always<\/i> complain about how badly past-you has documented things) though that\u2019s 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.<\/p>\n<p class=\"\">This brings us to the <code>__init__<\/code> method. This, as you know, is the method that\u2019s 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\u2019re passed at creation time (our convention is that all cogs, when instantiated, get passed in that bot object). If we don\u2019t do this we won\u2019t have easy access to this object, and since we stash <i>all<\/i> the bot state in that bot object that\u2019d be really annoying. So we keep it.<\/p>\n<p>Now, you may be thinking \u201cwait, the bot object must have a handle on all the cogs, so if the cog has a handle on the bot isn\u2019t that a circular handle-on-things loop and aren\u2019t those bad?\u201d and if you are, great! That is indeed the case, there is a circular reference. It\u2019s fine, though \u2014 that bot object lives as long as the bot does (so that\u2019s OK), and as we\u2019ll 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.<\/p>\n<p class=\"\">Alternately, if you weren\u2019t thinking about circular references then this is a good example of them and something to think about in the future. Circular references aren\u2019t <i>bad<\/i>, necessarily, but they are something to keep in mind because it can mean an object we think is dead (and, indeed, may actually <i>be<\/i> dead) \u201clives\u201d 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\u2019ll talk about this more when we talk about databases, but that\u2019s something for a future post or three.<\/p>\n<p>The second thing we do in this method is set the hidden attribute to False. This attribute is our general purpose \u201cis this cog for any user? Or is it for admins\/mods only?\u201d flag, and any cog with hidden set to True is assumed to contain <i>only<\/i> commands and event listeners that are\u2026 not exactly secret, but not for general server member use. The most obvious result of this is that cogs marked hidden=True won\u2019t show in the help output for regular users. (They still show for members that have mod privileges)<\/p>\n<h2><b>Cog registration<\/b><\/h2>\n<p>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\u2019s a setup function in it, and then calls that setup function, passing in a single parameter, the object representing the bot.<\/p>\n<p class=\"\">If your file doesn\u2019t have this then the cog won\u2019t be loaded, and the framework will yell at you telling you that it\u2019s missing. So that\u2019s nice. Note that this functions is a package-level function \u2014 that is, it\u2019s <i>not<\/i> a method inside the cog\u2019s 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\u2019ll get yelled at when loading the cog.<\/p>\n<p class=\"\">This particular setup function is <i>extremely<\/i> simple, as most of them are. There\u2019s just an <code>await bot.add_cog(TestCog(bot))<\/code> inside, which creates an object for our cog (the <code>TestCog(bot)<\/code> bit) and then registers it as a cog with the bot (the await <code>bot.add_cog()<\/code> bit). The add_cog method is a co-routine, which is why we await it. (We\u2019ll talk about await and coroutine-ness in a little while)<\/p>\n<p class=\"\">It\u2019s possible to define multiple cog-derived classes in a single file, and register two or more of them in the setup coroutine. Don\u2019t do that, though, for our bot we go with a one-cog-per-file setup.<\/p>\n<h2><b>Commands in the cog<\/b><\/h2>\n<p>This is where we (finally!) have some interesting stuff \u2014 the actual commands! Woo!<\/p>\n<p class=\"\">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\u2019t import all the way down into the commands namespace but that\u2019s fine, it\u2019s the same thing) Any co-routine (that\u2019s a function defined with async def rather than just def) with that decorator will get loaded into the bot\u2019s command table.<\/p>\n<p>The command-registry decorator has a number of <i>optional<\/i> named properties. We even set two of them \u2014 name and aliases.<\/p>\n<p class=\"\">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 <i>isn\u2019t<\/i> specified then the command name will be the same as the name of the co-routine that has the @command decorator. But don\u2019t do that, it\u2019s annoying. (We actually do this in a couple of spots and, indeed, is annoying. And I should go fix it)<\/p>\n<p class=\"\">aliases sets some <i>alternative<\/i> names for the command. For root commands generally we don\u2019t set aliases (it eats up the space of useful names) but they are useful for command groups. Given we\u2019re not covering groups here that\u2019s not a big deal so you can ignore that for the moment.<\/p>\n<p class=\"\">The method being decorated <i>must<\/i> be marked async \u2014 this is a requirement of the framework. (For several good reasons) We\u2019ll 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.<\/p>\n<p class=\"\">Every command method takes at <i>least<\/i> two parameters. The first is <code>self<\/code>, since this is a method. The second, <code>ctx: commands.Context<\/code>, is a <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/api.html?highlight=context#discord.ext.commands.Context\"><i>context<\/i><\/a> parameter. This is something the framework passes into every command handler, and it contains the context for the command. (Which, y\u2019know, you might expect from the fact that it\u2019s 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.<\/p>\n<p class=\"\">What\u2019s nice about contexts is they\u2019re <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/api.html#discord.abc.Messageable\">Messageables<\/a>. 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\u2019s what we do here in our little test program \u2014 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.<\/p>\n<p class=\"\">The one important thing here to note is that send, and reply, are <i>coroutines<\/i>. That has a lot of different ramifications that we\u2019ll talk about another time, but the single biggest one is that we can\u2019t just call the method with ctx.send(\u201csome text\u201d), but rather we <i>must<\/i> use await, and do await ctx.send(\u201csome text\u201d).<\/p>\n<p class=\"\">Why? The short answer is that when you call a co-routine (that is, a function or method marked <code>async<\/code>) you don\u2019t actually <i>run<\/i> the function at that point. Rather python hands you a little magic token that represents the function you want to call. When you <code>await<\/code> that token (as we do here as part of invoking the <code>send<\/code> method) <i>that\u2019s<\/i> when the function is actually run and does its thing.<\/p>\n<p class=\"\">Python will complain if it goes to destroy one of these coroutine tokens that <i>hasn\u2019t<\/i> been <code>await<\/code>ed. If you forget to <code>await<\/code> a coroutine (or, rather, <i>when<\/i> you forget, because you will \u2014 everyone does at some point) you\u2019ll see a complaint sent the console and in the error log.<\/p>\n<h2 class=\"\"><b>What\u2019s with the <\/b>unload_cog<b>?<\/b><\/h2>\n<p class=\"\">You may be wondering what\u2019s up with that <code>unload_cog<\/code> method that does exactly nothing. Why is it there and what\u2019s the point? It doesn\u2019t <i>do<\/i> anything, there\u2019s just a pass in there so python doesn\u2019t complain.<\/p>\n<p>You\u2019re right, it <i>doesn\u2019t<\/i> do anything, and indeed you could absolutely leave it out with no worries. Heck, a lot of our cogs don\u2019t even have one of these functions. This is part of the cog framework we use, though, so it\u2019s here as an illustration.<\/p>\n<p class=\"\">You\u2019ll remember that we said the cog framework we use allows is for cog unloading and reloading. <i>If<\/i> a cog has an unload_cog method then that method will get called as part of the unload process. It\u2019ll also get called as part of the <i>reload<\/i> process \u2014 the framework will call unload_cog on the old cog, and once that\u2019s done it\u2019ll instantiate the new cog.<\/p>\n<p class=\"\">In a lot of cases there\u2019s 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)<\/p>\n<p>Anyway, in this case there\u2019s really nothing there, just an empty method so we don\u2019t forget. (And for a place to add things later, which we will in future posts)<\/p>\n<h2><b>That\u2019s it for now<\/b><\/h2>\n<p class=\"\">And\u2026 that\u2019s it. A bare-bones little command for a discord.py based discord bot. Play around with this some and read the library docs \u2014 as you can see just from the raw size of them, there\u2019s a <i>lot<\/i> that you can do with your bot and this example is about as tiny as you can possibly get while still actually doing something.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>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\u2019m responsible for up to speed on what it does and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":3,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"federated","footnotes":""},"categories":[8,7,9],"tags":[6],"class_list":["post-67","post","type-post","status-publish","format-standard","hentry","category-bot","category-discord","category-python","tag-6"],"_links":{"self":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/67","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/comments?post=67"}],"version-history":[{"count":3,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/67\/revisions"}],"predecessor-version":[{"id":70,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/67\/revisions\/70"}],"wp:attachment":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/media?parent=67"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/categories?post=67"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/tags?post=67"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}