Bot commands with parameters

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’t take any parameters. There’s quite a lot you can do with those kinds of commands, but “quite a lot” is still pretty limiting. This time we’re going to talk about how to actually get data into our command so we can do stuff.

Simple parameters

We’ll start with the very basics — 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:

@commands.command(name=“echoer”)
async def echoer(self, ctx: commands.Context, param):
  await ctx.send(param)

Run that and the bot will send off the first word you gave to the command. Success!

If you try it and give the bot two words… it sends back just the first. Why is that?

Well, what’s happening here is that the discord.py library is tokenizing the command message the user sent, and sending each of those tokens into your command function. If you send extra tokens, things your command doesn’t 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’ll get yelled at, but that’s fine, it’s handy.

You may be thinking that the command maps words to parameters, but it doesn’t — the previous paragraph said tokens for a reason. discord.py respects quotes as it splits the command message into pieces — a quoted phrase counts as one token, so it’s 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’d think was the right thing. (Which is surprisingly annoying in practice, so it’s nice that it’s done for us) This isn’t a big deal now but is worth mentioning because it’s important in basically all the rest of the sections.

Typed parameters

Just taking in a string is fine, and you can do a lot with those since ultimately everything is just a string with some magic layered on top. Still, being a little more specific than “I need a string” is often handy, for example in many commands you’ll want one of those parameters to be a channel or a server member, or an emoji. The “easy” 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.

You might think about using python’s typed parameter system, which let you note what type of thing a function parameter is — that seems promising. You might also just discard that thought, since if you’ve tried typed parameters in regular python code you’ve probably noticed that they don’t actually do 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’t complain at all. Definitely annoying, and a little surprising.

Because of this you may have mostly written them off. Fair! (There are type checker programs you can run to see if you’ve done the right thing. We don’t use them, but they exist) But… turns out that in one specific circumstance type annotations have meaning and actually work!

The discord.py library does parameter type sniffing, and provides free typing for command functions. 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:

@commands.command(name=“double”)
async def doubler(self, ctx: commands.Context, param:int):
  await ctx.send(f”double that is {param*2}”)

And when your user invokes the command you know you’ve gotten an int so you don’t have to mess around with converting it. If the user passes in something that can’t be turned into an integer then the bot will complain at the user and our function won’t even get called.

You may be thinking here “that’s handy. Does this work for fancier things?” and the answer is yes. Yes it does. If you want an emoji, for example, you can do this:

@commands.command(name=“emojicheck”)
async def emojicheck(self, ctx: commands.Context, emoji:Emoji):
  await ctx.send(f”You gave me an emoji. Woo”)

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’d want — emojis, users, members, channels, threads, and servers. We can even set things up for objects that aren’t discord.py things using the converters API, but that’s a bit more advanced and the topic for another post.

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 ways to do better than that, but alas we don’t use them (not that we shouldn’t use them — we should! We just don’t) and that’s a bit of a more complex topic so we’ll cover it another time.

Optional or alternate parameters

The parameter sniffing that discord.py does is clever enough to pay attention to parameters you’ve annotated using the typing module. The two we use most are Optional and Union.

A parameter you’ve marked Optional is, as it says on the tin, optional. If the user leaves it out then it’s not an error, and the parameter is just set to None. For example:

@commands.command(name=“emojicheck”)
async def emojicheck(self, ctx: commands.Context, emoji:Optional[Emoji], word):
  if emoji is not None:
    await ctx.send(f”You gave me an emoji: {emoji}. Woo”)
  await ctx.send(f”You gave a parameter of {word}”)

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.

Some parameter types, like str, match pretty much everything and you won’t 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:

@commands.command(name=“say”)
async def say(self, ctx: commands.Context, where: Optional[TextChannel], what):
  if where is None:
    where = ctx.channel
  await where.send(what)

This example command has the bot say something. If the first parameter is a channel then that’s where the thing will be said, otherwise the bot will say it in the channel the command was issued in.

The other common type thing we use is the Union type. This lets you say “this parameter is one of these types” and the discord library will figure out what the user gave for us. Which is great! For example:

@commands.command(name=“tell”)
async def tell(self, ctx: commands.Context, where: Union[Member, TextChannel], what):
  await where.send(what)

What happens here is that if the user gave us a channel then we’ll send the message to that channel, while if they gave us a Member instead then we send the message to that server member. (Which turns into a DM, a handy thing to know)

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’s a str) but is surprisingly true for int, because it’s 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’ll get an int when the user actually gave you a member ID or something.

Parameter Collapsing

Up until now we’ve let discord.py split the command message into pieces for us, and we’ve taken one piece per parameter. But what if we don’t want the command message split up? Or what if we do but we want all the extra bits in one parameter?

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’s a list of all the remaining tokens, then prefix the parameter with an asterisk:

@commands.command(name=“echoer”)
async def echoer(self, ctx: commands.Context, *param):
  await ctx.send(param)

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’s technically a tuple, but close enough) The parameter will still be a tuple even if no values are available to fill in (because the user didn’t type that many in, for example) so it’s always safe to assume you can use array syntax to access the stuff that’s not in it.

The starred parameter must be the last 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’s generally kind of pointless as nothing will ever go in it. (Strictly speaking it could be a named parameter, but we don’t actually use those in our bot)

What, though, if you don’t 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’t like at least one of the smart quote styles that iOS tends to use), or just because it’s easier to grab everything as a single string since that’s what you need and are just going to .join(“ “) anyway?

That’s where rhe other potential way to get an ‘everything else’ parameter comes in. For this you throw a bare star in the parameter list with a single parameter after, and discord.py will throw everything left into that last parameter. More importantly, the library won’t tokenize anything, mangle anything, or strip anything — you’ll get exactly what the user typed, including any extra spaces or newlines or quotes or whatever.

Defaults

Python allows you to assign default values to a function parameter, and that’s carried over into discord.py command handlers. You can, if you choose, assign a default value to a parameter:

@commands.command(name="foo")
async def foo_cmd(self, ctx: commands.Context, param, otherparam="foobar"):
  await ctx.send(f"you have foo'd with {param} {otherparam}")

Regular python rules apply — parameters with defaults must come at the end of the list of parameters. They may be typed if you want, that’s fine.

If your default value is None then don’t actually do something like param: Emoji=None. Well, you can, but it’s much better to just mark optional parameters, which are None if not specified, as Optional, as we saw in the section on optional parameters.

Generally there aren’t that many reasons to have parameters with defaults for command functions. The “best” one, for some value of best, is the case where a function is both a registered command function and a function your code calls directly. In that case it’s 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)

Sniffing the message

We’ll mention this because it is an option, just in nearly every case it’s not a great one.

Whenever a command handler is called, the discord.py library passes in a context parameter. We’ve 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 message (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.

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.

You’ll almost never need any of this stuff, and generally re-doing the command line parsing is unnecessary, but it’s there if you need to be Very Clever. (Please don’t be Very Clever, though, it rarely ends well)

What about custom converters

You may, at this point, be thinking “wait, 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 datetime object or something?”

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’s worth most of the time, and also way beyond what we’re covering here. So we’re skipping that for now. (But if you want to read up on it, feel free)

So that’s parameters

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’t mean we don’t have to skip validation — just because we got a correctly constructed object doesn’t mean that what we got is actually fine — but at least it means we can skip having to do manual string-to-whatever conversions, which is nice.

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.

Continue reading

Setting up the ActivityPub plugin on a DreamHost VPS-hosted wordpress blog

You would think just enabling ActivityPub would be a matter of installing the AcivityPub plugin, turning it on, and you’er good! Right?

Yeah, well, things are never quite that easy. This blog is currently hosted on a DreamHost VPS (because I finally gave up hosting everything on my 2009 Mac Mini as it fell way out of support and leaving that open on the ‘net seemed… unwise) with WordPress now (RIP whatever the hell I was using back in 2005. Something SixApart-ish? I think?) and it should be just a matter of installing the plugin, turning it on, and it’s good!

Things are never that easy.

First problem, the plugin requires that the host username is the exposed username for per-user feeds. When you set up wordpress at DreamHost it chooses a somewhat… random name, which is generally a good idea (I’ve watched my logs in the past where penetration attempts just cycle through lists of common first and last names trying to get access to stuff) but annoying in this case. Moreso because you can’t change your username once it’s set up, and while there’s not too much on the blog there’s certainly more than I want to re-post or lose.

The fix there is, because this is a just-me blog, turn on a single global-blog-wide feed rather than per-user feed. Go to the settings tab on the ActivityPub setting screen (they’re meta-settings, I guess) and tick the “Enable blog” profile checkbox and give your blog the username you should have given yourself but didn’t. So that’s nice.

Handy protip from this — if you have multiple users on a blog and want them to have sane account names for syndication, give ’em sane usernames to start.

Second problem is that search didn’t work and when I tried looking for it on my home instance (yay, https://weatherishappening.network for all your slightly surreal Providence and environs weather forecasts and reminders to repent to your weather lords) because this requires WebFinger setup. I installed the web finger plugin (which is noted to not be tested with the version of WordPress I’m using but whatever) but that didn’t work because DreamHost does… things, and stuff, with .well-known, which is annoying. As a result there wasn’t an entry in my top level .well-known directory for web finger.

The solution there is to add a rewrite rule to the top-level .htaccess file for the site. Courtesy of this bug update I added:

RewriteRule ^.well-known/(host-meta|webfinger|nodeinfo|x-nodeinfo) /blog/.well-known/$1 [L]

(yes, including the [L] at the end for reasons I don’t know) to my .htaccess file and that made things work.Yay! Searchable, and subscribable, and all that. Dunno if comments work (I guess I’ll find out soon) but there’s a feed and that’s excellent. I assume that if your blog has a root that isn’t /blog (mine is, so that’s nice) you’d want to update that. It’s also possible that you don’t need to do this at all if WordPress controls your entire site from / on down.

So, in summary, for DreamHost VPS-hosted WordPress blogs:
1) Install ActivityPub and WebFinger plugins
2) Turn on a full-blog feed
3) Edit your top-level .htaccess file to redirect web finger (and some other stuff) to your blog’s root directory .well-known directory

Hopefully this’ll help someone else who might be poking at things and wondering WTF exactly isn’t working.

No-churn non-dairy ice cream

(Or, rather, no-churn potentially non-dairy frozen custard. Close enough)

tldr: pastry cream + swiss meringue + cold. Freeze and enjoy!

I occasionally get an urge to make ice cream, but to date it’s never been strong enough to bother buying an ice cream machine — I’ve had those in the past and they’re frankly too much hassle for the utility, especially when I have a J.P. Licks within reasonable walking distance. Still, the idea of a no-churn ice cream is tempting, and I was wondering what I could do to make some when I wanted. Sure, there’s semifreddo which is a good enough option, but that requires having heavy cream around and I often don’t.

Thinking about ice cream got me thinking about one of its relatives, frozen custard (which is awesome), and that got me thinking that custard is almost pastry cream, which I do a bunch, pastry cream is easy and also potentially non-dairy, and I pretty much always have the ingredients around.

Just making and freezing pastry cream isn’t great because it’s kinda solid (turns out that all that air churned into ice cream really is important) so you need air in there. Whipped cream is useful for this but, as previously noted, I don’t usually have cream around for whipping.

My first thought was that pastry cream maybe could be whipped enough, but nope. It’s not like ganache where you can get a lot of air in, which isn’t surprising. (Fine, maybe with a lot of corn starch or added fat, but not base pastry cream) But… one of the things left over from pastry cream making is egg white, and with egg whites you can make meringue. Which is mostly air and traditionally used as a lightener for things. Turns out that if you make a batch of pastry cream then make a batch of Swiss meringue with the leftover whites and mix it in that you get a really rich, luscious, tasty frozen dessert. Score!

The recipe:


Pastry cream


350g liquid
three egg yolks (50g(ish), but whatever, close enough)
20g corn starch
75g sugar
(optional) splash of flavoring like vanilla

Toss everything into a pot. Stirring the whole damn time, because burnt pastry cream is awful, you bring this to a sputtering boil and cook or two minutes. (you really need to just make sure everything hits 75c/170f to neutralize the starch-munching enzymes in the egg yolks, though freezing makes all this much more forgiving)

Pop this into something that will let it cool quickly (I use a cake pan), cover with some plastic wrap or something so you don’t get a skin, and cool. Fridge-cool is best, but room temperature is tolerable.

While that’s cooling make the meringue which is stupid easy:

Swiss meringue


three egg whites
25g sugar (two tablespoons)
2g cream of tartar (optional)

Mix these in a metal bowl and heat over a bain marie until it hits 71c/160f. Take off the heat and whip to soft peaks.

When the pastry cream is cool-ish, whip it up as best you can (which won’t be great but give it a go for 30 seconds or so), fold in the egg whites and stir to combine. Put some plastic wrap on top for skin-prevention safety, drop everything into the freezer, and freeze until it’s solid.

Now, traditionally pastry cream is made with milk but it’s 100% fine to substitute any kind of liquid. Want orange “ice cream”? Use OJ. Want lemon? Use lemon juice. (I’d add a bit more sugar, and a couple of grams of baking soda to cut the acid, but you do you) Want Bailey’s Irish Cream flavor? You’d fit in well with a bunch of lawyers I know and also that’s probably awesome. (Might want to set your freezer a little colder for that) Anyway, choose your liquid, and if it’s not milk based then this is dairy free for a bonus.

Anyway, there you go. Pastry cream + swiss meringue, mix, freeze.

It does have a bit of a cooked flavor because, well, it is. Since it’s frozen you can probably get away with just heating it up until it gets thick rather than fully cooking it — I suspect (though I haven’t checked) that it will freeze up well before the amylases in the egg yolks liquefy the starches and turn it to goo.

As a bonus it’s even kinda lower-fat/lower-calorie than actual ice cream — there’s relatively little fat in the pastry cream itself, and none in the meringue. (That’s kind of beside the point, though.)

Lazy bread!

Preheating ovens is a waste of time

It’s snowing here today, so I figured it was an excellent day for stew and fresh crusty bread. And, as I didn’t have any fresh crusty bread it was time to make some. So I did.

This is a simple 65% loaf, with 500g of flour. No big deal, pretty standard, but the thing here is I got really lazy baking it. I’d heard that you could skip the oven/dutch oven preheating step and just toss the loaf into the oven cold. Since the loaf had already risen well before baking I figured my worst case was an OK loaf, but… this one worked out really, really well.

Baked in a 450F oven on fan, inside a french oven. (Which is just a dutch oven only without the lip on the lid to hold coals) Put the loaf into the cold pot, put the pot in a cold oven, turned on the heat, and let it go for an hour. Then I took the lid off and let it go for another 10 minutes to finish.

This worked great. Seriously, lovely loaf, the crust was all crackly while I took this pic and I’m quite looking forward to dinner and a chunk of this. I don’t think I’m going to bother preheating the oven ever again, at least not for simple lean loaves like this.

Also, entirely separately, the Fellow Ode grinder in the background remains awesome. Silt-free french press coffee is lovely and my only regret is not getting a nice grinder earlier on in our communal zombie apocalypse.

 

German Buttercream is awesome

Really really lemon cake. Seriously, that frosting is about 1/3 lemon juice by weight. Yes, I know, both my food photography and food styling skills need work. Gimme a break, we’re in a zombie apocalypse, I don’t get much practice.

The recipe

German buttercream frosting is one of the four and a half main kinds of buttercream frostings, and one that’s not as common here in the US as it really ought to be, because it’s both really easy and you can pack it full of flavor.

The recipe is simple:

  • 1 pound room temperature (~65F/19C) butter
  • 1 batch of pastry cream

Beat the butter until it’s light and fluffy. Beat it a lot. Once it’s fluffy start adding in the pastry cream one spoonful or so at a time, beating to incorporate. When all the pastry cream is added and the frosting is fully beaten (it’ll be a normal buttercream frosting consistency, fluffy and reasonably spreadable) then you’re done.

If your butter is too cold the frosting will be a bit thick and hard to work with, and if it’s too warm it’ll be thin and a little loose. Like with all other buttercreams, or most anything else that uses butter, temperature is important. If you’re somewhere warm, add extra corn starch to the pastry cream (it can handle up to about twice what my linked recipe has) which will make the pastry cream nearly solid but lightens up with the butter and helps offset higher temperatures.

This is enough to frost an 8″ or 9″ round cake. It might be a bit tight with a 9″ if you frost the middle, too, but seriously don’t do that — the middle layer should be something else that complements the cake and buttercream. (In mine here it’s lemon curd)

Why German Buttercream’s great

German buttercream frosting is great because it’s so simple, doesn’t require molten sugar, and you can make up the pastry cream ahead of time. If I want an actual white frosting I’d use something else (probably a Swiss buttercream made with goat butter) but for anything else? This is my current go-to frosting.

The other thing you have going for you here is that you can pack a huge amount of flavor into the frosting. Most buttercream frostings are buttery and sweet, but only mildly flavored. Some people like that but I prefer something with a bit more presence in the cake, and that’s what you have here. In the pic the (somewhat amateurish) cake is frosted with a buttercream that’s about 1/3 lemon juice by weight. It’s intensely lemon, and even though I added a bunch of sugar (almost 200g of powered sugar) it’s still really sour and it’s just awesome.

It’s really hard to get anything even close to that intense a flavor from other kinds of buttercreams. In some cases, where your flavoring agent is dry and intensely flavored like with freeze-dried berries, you can manage but it’s hard and you’re limited in the flavors available to you. For a German Buttercream, as long as your intense flavor is liquid you can go wild with it.

An apple spice cake with apple-cider frosting? Just make your pastry cream with cider and that’d be awesome and now I want it. Cranberry frosting for a chocolate-cranberry cake? (Or, at least, a  chocolate cake with a cranberry chutney filling and cranberry-juice-based frosting) Nice.

Once the office re-opens I fully intend on making a rich chocolate cake and a Jameson’s frosting. (Which, if I math out right, will be about 10% ABV (maybe a little less, there’s air in that volume), so eat responsibly when it happens)

 

 

Generic pastry cream

Pastry cream is a simple starch-stabilized variant of a custard. Traditionally it’s vanilla flavored, with some sugar and faffing around with tempering your eggs and such, but it turns out that’s not actually necessary.

350g liquid (~12 fluid oz)
50g egg yolk (~3 yolks)
22g corn starch (I dunno how much, weight it)

Optional: sugar (75g gives a reasonable sweetness most of the time)