{"id":74,"date":"2024-06-17T22:08:19","date_gmt":"2024-06-18T02:08:19","guid":{"rendered":"https:\/\/www.sidhe.org\/blog\/?p=74"},"modified":"2024-06-17T22:08:19","modified_gmt":"2024-06-18T02:08:19","slug":"bot-commands-with-parameters","status":"publish","type":"post","link":"https:\/\/www.sidhe.org\/blog\/2024\/06\/17\/bot-commands-with-parameters\/","title":{"rendered":"Bot commands with parameters"},"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<p>Last week we talked about the basic structure of a discord.py cog, and built a very simple command. Fun! The one downside there was the command didn\u2019t take any parameters. There\u2019s quite a lot you can do with those kinds of commands, but \u201cquite a lot\u201d is still pretty limiting. This time we\u2019re going to talk about how to actually get data into our command so we can do stuff.<\/p>\n<p class=\"\"><b>Simple parameters<\/b><\/p>\n<p class=\"\">We\u2019ll start with the very basics \u2014 just getting in a parameter. That turns out to be easy. discord.py does Very Clever Things and snoops the declaration of your command function, and if it sees that it has parameters then it chunks up the command the user issued and passes those chunks into your function. So, if you want a command that takes a single parameter, you just declare it like:<\/p>\n<blockquote class=\"\">\n<pre class=\"\">@commands.command(name=\u201cechoer\u201d)\r\nasync def echoer(self, ctx: commands.Context, param):\r\n  await ctx.send(param)<\/pre>\n<\/blockquote>\n<p>Run that and the bot will send off the first word you gave to the command. Success!<\/p>\n<p>If you try it and give the bot <i>two<\/i> words\u2026 it sends back just the first. Why is that?<\/p>\n<p>Well, what\u2019s happening here is that the discord.py library is <i>tokenizing<\/i> the command message the user sent, and sending each of those tokens into your command function. If you send <i>extra<\/i> tokens, things your command doesn\u2019t say it wants, those extra tokens are just discarded. This is a little different than with python, where if you call a function with too many parameters you\u2019ll get yelled at, but that\u2019s fine, it\u2019s handy.<\/p>\n<p>You may be thinking that the command maps <i>words<\/i> to parameters, but it doesn\u2019t \u2014 the previous paragraph said <i>tokens<\/i> for a reason. discord.py respects quotes as it splits the command message into pieces \u2014 a quoted phrase counts as one token, so it\u2019s common to throw quotes around multi-word phrases that should count as a single parameter. The library is generally good about quote types, and does what you\u2019d think was the right thing. (Which is surprisingly annoying in practice, so it\u2019s nice that it\u2019s done for us) This isn\u2019t a big deal now but is worth mentioning because it\u2019s important in basically all the rest of the sections.<\/p>\n<p><b>Typed parameters<\/b><\/p>\n<p class=\"\">Just taking in a string is fine, and you can do a lot with those since ultimately <i>everything<\/i> is just a string with some magic layered on top. Still, being a little more specific than \u201cI need a string\u201d is often handy, for example in many commands you\u2019ll want one of those parameters to be a channel or a server member, or an emoji. The \u201ceasy\u201d thing to do is take in a string and then do the conversion in our command code, which is 100% OK but a lot of work and kind of annoying.<\/p>\n<p class=\"\">You might think about using python\u2019s <i>typed parameter<\/i> system, which let you note what type of thing a function parameter is \u2014 that seems promising. You might also just discard that thought, since if you\u2019ve tried typed parameters in regular python code you\u2019ve probably noticed that they don\u2019t actually <i>do<\/i> anything. That is, if you mark a parameter as being an int but pass in a string (or a bool, or a Bot object, or whatever) python is 100% good with that and doesn\u2019t complain at all. Definitely annoying, and a little surprising.<\/p>\n<p>Because of this you may have mostly written them off. Fair! (There are type checker programs you can run to see if you\u2019ve done the right thing. We don\u2019t use them, but they exist) But\u2026 turns out that in one specific circumstance type annotations have meaning and actually work!<\/p>\n<p>The discord.py library does parameter type sniffing, and provides free typing for <i>command functions<\/i>. If you mark a parameter as an int, the discord library will auto convert it to an int for you. It means that you can define a command like:<\/p>\n<blockquote>\n<pre class=\"\">@commands.command(name=\u201cdouble\u201d)\r\nasync def doubler(self, ctx: commands.Context, param:int):\r\n  await ctx.send(f\u201ddouble that is {param*2}\u201d)<\/pre>\n<\/blockquote>\n<p>And when your user invokes the command you know you\u2019ve gotten an int so you don\u2019t have to mess around with converting it. If the user passes in something that can\u2019t be turned into an integer then the bot will complain at the user and our function won\u2019t even get called.<\/p>\n<p>You may be thinking here \u201cthat\u2019s handy. Does this work for fancier things?\u201d and the answer is yes. Yes it does. If you want an emoji, for example, you can do this:<\/p>\n<blockquote class=\"\">\n<pre class=\"\">@commands.command(name=\u201cemojicheck\u201d)\r\nasync def emojicheck(self, ctx: commands.Context, emoji:Emoji):\r\n  await ctx.send(f\u201dYou gave me an emoji. Woo\u201d)<\/pre>\n<\/blockquote>\n<p class=\"\">The emoji variable here will have a discord.Emoji object in it, which is awesome and useful. This works for pretty much all the discord.py objects you\u2019d want \u2014 emojis, users, members, channels, threads, and servers. We can even set things up for objects that <i>aren\u2019t<\/i> discord.py things using the <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/commands.html#converters\">converters API<\/a>, but that\u2019s a bit more advanced and the topic for another post.<\/p>\n<p>You may have noticed that the error messages that pop up when the user gives us the wrong kind or parameter are kind of not great. There are <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/commands.html#error-handling\">ways to do better than that<\/a>, but alas we don\u2019t use them (not that we shouldn\u2019t use them \u2014 we should! We just don\u2019t) and that\u2019s a bit of a more complex topic so we\u2019ll cover it another time.<\/p>\n<p><b>Optional or alternate parameters<\/b><\/p>\n<p>The parameter sniffing that discord.py does is clever enough to pay attention to parameters you\u2019ve annotated using the<a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/commands.html#special-converters\"> typing module<\/a>. The two we use most are <a href=\"https:\/\/docs.python.org\/3\/library\/typing.html#typing.Optional\">Optional<\/a> and <a href=\"https:\/\/docs.python.org\/3\/library\/typing.html#typing.Union\">Union<\/a>.<\/p>\n<p>A parameter you\u2019ve marked Optional is, as it says on the tin, optional. If the user leaves it out then it\u2019s not an error, and the parameter is just set to None. For example:<\/p>\n<blockquote class=\"\">\n<pre class=\"\">@commands.command(name=\u201cemojicheck\u201d)\r\nasync def emojicheck(self, ctx: commands.Context, emoji:Optional[Emoji], word):\r\n  if emoji is not None:\r\n    await ctx.send(f\u201dYou gave me an emoji: {emoji}. Woo\u201d)\r\n  await ctx.send(f\u201dYou gave a parameter of {word}\u201d)<\/pre>\n<\/blockquote>\n<p>In this case if the user does something like `emojicheck :hammer: time then the emoji variable will have the hammer emoji in it. If they just did something like `emojicheck time then the emoji variable will be set to None.<\/p>\n<p class=\"\">Some parameter types, like str, match pretty much everything and you won\u2019t see them set to None very often. Other types, like Emoji or Member, can often be None. One of the ways we use optional parameters are those cases where we might take a destination, or might not:<\/p>\n<blockquote>\n<pre class=\"\">@commands.command(name=\u201csay\u201d)\r\nasync def say(self, ctx: commands.Context, where: Optional[TextChannel], what):\r\n  if where is None:\r\n    where = ctx.channel\r\n  await where.send(what)<\/pre>\n<\/blockquote>\n<p>This example command has the bot say something. If the first parameter is a channel then that\u2019s where the thing will be said, otherwise the bot will say it in the channel the command was issued in.<\/p>\n<p>The other common type thing we use is the Union type. This lets you say \u201cthis parameter is one of these types\u201d and the discord library will figure out what the user gave for us. Which is great! For example:<\/p>\n<blockquote>\n<pre class=\"\">@commands.command(name=\u201ctell\u201d)\r\nasync def tell(self, ctx: commands.Context, where: Union[Member, TextChannel], what):\r\n  await where.send(what)<\/pre>\n<\/blockquote>\n<p>What happens here is that if the user gave us a channel then we\u2019ll send the message to that channel, while if they gave us a <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/api.html#discord.Member\">Member<\/a> instead then we send the message to that server member. (Which turns into a DM, a handy thing to know)<\/p>\n<p class=\"\">Ordering of a Union is important, because the discord library tries each type in turn until the parameter matches properly. This means that more general types like int or str should come at the end of the list while more specialized types should come first. This is obviously true for str (everything\u2019s a str) but is surprisingly true for int, because it\u2019s entirely fine to refer to a user, or a member, or a channel, or a server, or a message, as a plain int. (Everything in discord has a unique integer attached to it, and no two things have the same integer) Not a big deal, just if a parameter could be a member or an int the type annotation should be Union[Member,int] otherwise there are cases where you\u2019ll get an int when the user actually gave you a member ID or something.<\/p>\n<p><b>Parameter Collapsing<\/b><\/p>\n<p>Up until now we\u2019ve let discord.py split the command message into pieces for us, and we\u2019ve taken one piece per parameter. But what if we <i>don\u2019t<\/i> want the command message split up? Or what if we do but we want all the extra bits in one parameter?<\/p>\n<p>Turns out that, for this, discord.py does just what regular python does, which is handy. So, for example, if you want a parameter that\u2019s a list of all the remaining tokens, then prefix the parameter with an asterisk:<\/p>\n<blockquote>\n<pre class=\"\">@commands.command(name=\u201cechoer\u201d)\r\nasync def echoer(self, ctx: commands.Context, *param):\r\n  await ctx.send(param)<\/pre>\n<\/blockquote>\n<p>If you do this then discord.py will tokenize the whole command message and splat any remaining tokens into that starred parameter where you can access them using regular array access syntax. (It\u2019s technically a tuple, but close enough) The parameter will still be a tuple even if <i>no<\/i> values are available to fill in (because the user didn\u2019t type that many in, for example) so it\u2019s always safe to assume you can use array syntax to access the stuff that\u2019s not in it.<\/p>\n<p>The starred parameter must be the <i>last<\/i> parameter in the parameter list, because it gathers up all the tokens left. You can, if you really, want, put something after your starred parameter but it\u2019s generally kind of pointless as nothing will ever go in it. (Strictly speaking it could be a named parameter, but we don\u2019t actually use those in our bot)<\/p>\n<p class=\"\">What, though, if you <i>don\u2019t<\/i> want discord.py to tokenize things for you? Maybe because whitespace is important (the standard tokenization eats newlines), or because the quote-detection system sometimes freaks out (it doesn\u2019t like at least one of the smart quote styles that iOS tends to use), or just because it\u2019s easier to grab everything as a single string since that\u2019s what you need and are just going to .join(\u201c \u201c) anyway?<\/p>\n<p>That\u2019s where rhe <i>other<\/i> potential way to get an \u2018everything else\u2019 parameter comes in. For this you throw a bare star in the parameter list with a single parameter after, and discord.py will throw <i>everything<\/i> left into that last parameter. More importantly, the library won\u2019t tokenize anything, mangle anything, or strip anything \u2014 you\u2019ll get <i>exactly<\/i> what the user typed, including any extra spaces or newlines or quotes or whatever.<\/p>\n<p><b>Defaults<\/b><\/p>\n<p>Python allows you to assign default values to a function parameter, and that\u2019s carried over into discord.py command handlers. You can, if you choose, assign a default value to a parameter:<\/p>\n<blockquote class=\"\">\n<pre class=\"\">@commands.command(name=\"foo\")\r\nasync def foo_cmd(self, ctx: commands.Context, param, otherparam=\"foobar\"):\r\n  await ctx.send(f\"you have foo'd with {param} {otherparam}\")<\/pre>\n<\/blockquote>\n<p class=\"\">Regular python rules apply \u2014 parameters with defaults must come at the end of the list of parameters. They may be typed if you want, that\u2019s fine.<\/p>\n<p><i>If<\/i> your default value is None then don\u2019t actually do something like param: Emoji=None. Well, you can, but it\u2019s much better to just mark optional parameters, which are None if not specified, as Optional, as we saw in the section on optional parameters.<\/p>\n<p>Generally there aren\u2019t that many reasons to have parameters with defaults for command functions. The \u201cbest\u201d one, for some value of best, is the case where a function is both a registered command function <i>and<\/i> a function your code calls directly. In that case it\u2019s not unreasonable to have a default parameter or two. (We do this for some of our commands that can be called either in their normal form or in a test form, though in retrospect that may have not been the best decision)<\/p>\n<p class=\"\"><b>Sniffing the message<\/b><\/p>\n<p>We\u2019ll mention this because it <i>is<\/i> an option, just in nearly every case it\u2019s not a great one.<\/p>\n<p>Whenever a command handler is called, the discord.py library passes in a context parameter. We\u2019ve seen this in all the examples so far. That context has a lot of different bits of information attached to it, and one of them is the <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/api.html#discord.ext.commands.Context.message\">message<\/a> (an object of type Message) that triggered the command. You can, if you want, look and see exactly what the user sent and pull anything out of it that you like.<\/p>\n<p>The context also has a few other bits of useful information, such as the command string that the framework thinks invoked the command (handy if a command has aliases and you want to see if they were what the user used), parent command strings for multi-word commands, the actual Command object attached to the command, and more.<\/p>\n<p class=\"\">You\u2019ll almost never need any of this stuff, and generally re-doing the command line parsing is unnecessary, but it\u2019s there if you need to be Very Clever. (Please don\u2019t be Very Clever, though, it rarely ends well)<\/p>\n<p><b>What about custom converters<\/b><\/p>\n<p>You may, at this point, be thinking \u201cwait, if discord.py has Magic to turn something into a Member object does that mean we can have our own magic to convert into, I dunno, a <a href=\"https:\/\/docs.python.org\/3\/library\/datetime.html\">datetime<\/a> object or something?\u201d<\/p>\n<p>And the answer is yes! Yes we can, there are absolutely ways to do this. We even do them in a couple of places, but frankly it turns out to be more trouble than it\u2019s worth most of the time, and also way beyond what we\u2019re covering here. So we\u2019re skipping that for now. (But if you want to <a href=\"https:\/\/discordpy.readthedocs.io\/en\/stable\/ext\/commands\/commands.html#converters\">read up on it<\/a>, feel free)<\/p>\n<p><b>So that\u2019s parameters<\/b><\/p>\n<p class=\"\">This is the basics of parameters, and with this you can set up command functions so the discord library does 95% of the drudgework of typing for us. It doesn\u2019t mean we don\u2019t have to skip validation \u2014 just because we got a correctly constructed object doesn\u2019t mean that what we got is actually fine \u2014 but at least it means we can skip having to do manual string-to-whatever conversions, which is nice.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Last week we talked about the basic structure of a discord.py cog, and built a very simple command. Fun! The one downside there was the command didn\u2019t take any parameters. There\u2019s quite a lot you can do with those kinds of commands, but \u201cquite a lot\u201d is still pretty limiting. This time we\u2019re going to [&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":[],"class_list":["post-74","post","type-post","status-publish","format-standard","hentry","category-bot","category-discord","category-python"],"_links":{"self":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/74","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=74"}],"version-history":[{"count":1,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/74\/revisions"}],"predecessor-version":[{"id":75,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/posts\/74\/revisions\/75"}],"wp:attachment":[{"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/media?parent=74"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/categories?post=74"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sidhe.org\/blog\/wp-json\/wp\/v2\/tags?post=74"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}