Building a Discord Bot in PowerShell and Azure Functions

An example of how I made a working Discord bot running on just Azure Functions!

Background

I like using Discord to accomplish stuff. But I’m no programmer. That leaves me with little space to operate in; making a Discord bot is hardâ„¢. I’ve made some scripts for my own usage that integrate with Discord bots, but they’ve always been for my own use only.

However, my April Fools Day guild is chasing through a pre-gameday challenge and needed a Discord bot that would lookup/return tinyurl links. I decided that this was the time that I was going to get a real Discord bot running for their use. And today, I am happy to present my bot, Expando!

Pre-requisites

This post is a “how-do” or “how-did-I” and not a “how-to”, which means that I am not trying to make a comprehensive walkthrough. In order to get the most effect from this post, you should have familiarity (or go get some first) with the following:

  1. PowerShell
  2. Azure Functions/APIs
  3. Familiarity with Discord and Discord servers (called Guilds in docs)

If you want to follow along with my code, its up in Github.

Crafting the Architecture

Traditionally, Discord bots are long-running services that maintain a connection to Discord, so that Discord can tell the bot when stuff is happening. This enables a lot of cool stuff with intents, but its also the opposite of Azure Functions. Azure Functions are supposed to be short lived processes that don’t maintain any state with the Discord servers. Enter Interactions.

I want to leave a big thanks to SoTrx’s repo for guiding me through a lot of the ideas presented here.

Understanding Discord Interactions

Discord interactions are simply webhooks that Discord can generate and send to your bot service. No longer do you have to maintain a persistent connection for event-driven bots! This pairs wonderfully with Azure Functions. Now you can make your API however you want and then let Discord call your function anytime that someone uses it.

Interactions really are the magic sauce I’ve been wanting for a long time to be able to do a full Discord bot in Azure Functions. But there are some things to be aware of when it comes to using Interactions for a bot…

Using Interactions

Interactions are meant to be FAST. You have 3s to reply to an interaction. You can reply with “I got it, here’s your data” or even a “I got it, gimme 15 minutes to reply.” This pattern of doing the provisional reply shows up on the user end as “ is typing…”.

!! ACHTUNG !! Cold Starts on Azure are rough in PowerShell. It takes 7s+ on average according to this blog to boot up a PowerShell function. That’s been my experience as well. You either need an Azure Function with always on capability (sometime called always warm) or you need to have your functions to be active enough that they never have cold starts.

You can of course shortcut this issue by having a timer function that runs every 5 or 10 minutes that does nothing at all other than start up your function.

Additionally, you must authenticate the interaction request via Ed25519 signature verification. Ed25519 signature verification isn’t something that PowerShell has anything built in for, so you will need to pull from something else to do it. I grabbed a little DLL (Rebex.Ed25519.dll) that had 0 dependancies and dropped it into my project.

This dll made it easy to create a function to assert that a valid signature (Assert-Signature) was sent with the request.

Even when my functions were warm, resolving the tinyurl redirect still sometimes took too long and caused my bot to timeout that 3s window. Version 2 of the Azure Function solves this by chaining together two functions.

Function Chaining

Because I need to respond quickly, I settled on using two azure functions for the bot. The first one will verify the request and respond provisionally to satisfy Discord and also push the input data to an Azure Queue. The 2nd function will get triggered by that queue and do the real work for results.

This 2nd function can craft a nice and full webhook with embedded cards and whatnot, but I opted to go with the simple content property that works for plaintext responses.

Writing Durable Functions

I could have written these functions as a single Durable Function. I may well re-write the bot as a durable function just for fun. It would look pretty similar to the existing chaining.

Ok, now lets talk about enabling interactions on Discord!

Enabling Interactions

When you create a bot in Discord, in the general tab there is a field for the interaction endpoint URI. In order to enter save a URI in that field, you must already have your Azure Function up and running and available for usage. That includes the signature verification piece. If you can’t save the field, then you don’t have everything configured right.

Discord periodically will send test pings to your interaction endpoint just to make sure that you are still doing signature validations and to ensure that your bot is still live. Its a critical piece to get right!

Registering Commands

Once I had the interaction stuff figured out, I needed to register commands in Discord so that it would know what to send to my interaction endpoint. The nice thing about application commands is that they enable Discord provide a rich command experience for users. There are three types of commands you can register:

  1. Slash commands (commands you run from the chat box)
  2. User commands (commands you run from the bot user)
  3. Message commands (commands you run on messages in chat)

All of these got to the same endpoint URI and can be used in a DM with your bot (if users have 1 mutual server with the bot). DMs to the bot do present some data differently, so its worth trying out both ways when you make a command.

In order to register a command, I needed to use my app ID and my app client_secret to get a bearer token. Then I could query existing commands or add commands to my test server (and later, globally once I was done testing).

Adding the bot to a server

Your bot needs some permissions in order to work on a server. You craft a URL generator from your application OAuth2 page. I dunno exactly what permissions are needed, but I found that applications.commands and bot->Send Messages seem to work good.

Copy the link out from that page and use it to add your bot to a server that you control.

Trying out my bot

If you want to, you can add my bot to your server to see how well it performs. I will probably be deleting the app in the next few weeks.

Discussion

Discuss on reddit