Discord is moving its API more and more towards event-based interactions (webhooks) rather than relying on always-on connections (websockets). This is good news as the internet becomes increasingly serverless, but presents a new challenge for us serverless developers since guides on how to navigate the new APIs are still slim.
Fear not! In this article, we'll use Napkin to demystify how to set up a webhook-based slash command on Discord. To make things more interesting, our bot will actually do something cool: show us the current location of the International Space Station!
Here's how the bot interaction will look on Discord when we're done.
Want to skip ahead to the final code? You can check it out and fork it by following the links below. Or, continue on as we set everything up step-by-step.
Discord Bot Code
Still here? Awesome! 😄 Let's get started.
Initial Setup
First, we'll create a new Application in the Discord Developer Portal. Head over to https://discord.com/developers/applications and click "New Application" in the top right.
Next we'll add a Bot to our application. Go to
,
create a new one, and enable the Message Content Intent (seen at the bottom).
Lastly, copy the bot's Token by clicking the
button. We'll need it
for the next step.
Now we have to register a slash command for our new application. To do that, we have to send a HTTP request to the Discord API. You can fork this Napkin function to do that - just copy in your Application ID and Bot Token and run it. For more details on registering commands, check out Discord's official docs.
OK. Let's see what we have so far
Done
- Application created
- Bot created and configured
- Slash command registered
Still To Do
- Write the code
- Point our Application's webhook URL to our Napkin function
Writing the Code
Head over to napkin.io/dashboard/new and create a new Python function (for Javascript, see the example code here).
To start, we'll write the logic to verify Discord's security signatures. Discord uses these signatures to ensure that your bot is capable of identifying legit requests from Discord and denying unauthorized requests from evildoers on the internet.
Discord doesn't mess around here. It will actually try to trick your bot by sending bad signatures from time to time. If your bot fails to deny those requests, Discord takes it offline.
In order to verify those security signatures, Discord recommends using a popular crypto library like PyNacl, so we'll do just that. Let's also throw in the rest of the imports we'll need while we're at it.
from napkin import request, responseimport osimport uuidfrom nacl.signing import VerifyKeyfrom nacl.exceptions import BadSignatureError
OK, now the verification part which trips a lot of people (maybe just me) up. We'll
use the
module to verify the contents of the the request header sent from
Discord. If it's invalid, we respond with a
status. If it's valid,
we do our bot thing 🤖
# you can find your application public key at# https://discord.com/developers/applications/<my_app_id>/informationPUBLIC_KEY = os.environ['DISCORD_PUBLIC_KEY']
# The code below handles Discord's verification check for webhook-based interactions.# To read more about the Discord API's security requirements, see:# https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorizationverify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))signature = request.headers["X-Signature-Ed25519"]timestamp = request.headers["X-Signature-Timestamp"]body = request.body
message = message = timestamp.encode() + request.data.encode()try: verify_key.verify(message, bytes.fromhex(signature)) # If we get here without a BadSignatureError, we're good! response.status_code = 200
# response logic to go here...except BadSignatureError: # In this case, we failed verification due to one of the following: # ----------------------------------------------------------------- # - Discord is testing our bot to make sure we fail invalid signatures # - An unauthorized request is being made to our bot from somewhere on the internet response.status_code = 401 response.body = "Bad Signature"
Remember how I mentioned Discord sends requests to your bot just to ensure
it does verification properly? It does that via a certain type of HTTP request
that the API Reference calls a PING request. Request types from Discord are
marked in the request JSON body's
field. According to the docs, if we
receive a request where
equals 1, it's a PING request, and we should
respond with a simple JSON object containing
to acknowledge it.
# https://discord.com/developers/docs/interactions/receiving-and-respondingif body["type"] == 1: # type 1 is a Discord verification check (PING) response.body = {'type': 1}
With that code added, we can now set our webhook URL in the developer portal!
Set the Webhook URL
Make sure your code is deployed, then copy the function URL. Go back to the
Discord Developer Portal and paste the URL under
. Save changes and Discord
will test our bot to make sure it verifies signatures correctly. You should see
a confirmation message.
We can actually go to our Napkin function logs and see that Discord has performed
the test. Go to your function's Event Logs and you should see 1 or more PING
requests from Discord. If they really wanted to test you, you might even see
them sending a bad signature in one of the requests and your function responding
with a
.
Succesful PING request
Failed verification. Nice try, Discord.
OK. Time for the final piece. The actual slash command response. It's actually super simple.
if body["type"] == 1: # type 1 is a Discord verification check response.body = {'type': 1}
elif body["type"] == 2: # type 2 is a slash command interaction response.body = { "type": 4, # type 4 = CHANNEL_MESSAGE_WITH_SOURCE "data": { "embeds": [{ "title": ":red_circle: Current ISS Location", "image": { # add a random uuid argument to prevent Discord from caching image # You can see and fork the iss-map code at # https://www.napkin.io/n/61bb5e0bab3848a3 "url": f"https://napkin-examples.npkn.net/iss-map?id={uuid.uuid4()}" } }] } }
In the interest of separation of concerns,
I've split the code that plots the
position of the ISS into a separate function called
. If you're interested
in how that code works, feel free to check it out and fork it here! As for the bot
code, we simply include the iss-map function URL as part of the embed in our
response message.
The final code for the bot should now look like this.
from napkin import request, responseimport osimport uuidfrom nacl.signing import VerifyKeyfrom nacl.exceptions import BadSignatureError
# you can find your application public key at# https://discord.com/developers/applications/<my_app_id>/informationPUBLIC_KEY = os.environ['DISCORD_PUBLIC_KEY']
# The code below handles Discord's verification check for webhook-based interactions.# To read more about the Discord API's security requirements, see:# https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorizationverify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))signature = request.headers["X-Signature-Ed25519"]timestamp = request.headers["X-Signature-Timestamp"]body = request.body
message = message = timestamp.encode() + request.data.encode()try: verify_key.verify(message, bytes.fromhex(signature)) # If we get here without a BadSignatureError, we're good! response.status_code = 200
# what we do now depends on the Interaction Type that Discord sends # https://discord.com/developers/docs/interactions/receiving-and-responding if body["type"] == 1: # type 1 is a Discord verification check response.body = {'type': 1}
elif body["type"] == 2: # type 2 is a slash command interaction response.body = { "type": 4, # type 4 = CHANNEL_MESSAGE_WITH_SOURCE "data": { "embeds": [{ "title": ":red_circle: Current ISS Location", "image": { # add a random uuid argument to prevent Discord from caching image # You can see and fork the iss-map code at # https://www.napkin.io/n/61bb5e0bab3848a3 "url": f"https://napkin-examples.npkn.net/iss-map?id={uuid.uuid4()}" } }] } }except BadSignatureError: # In this case, we failed verification due to one of the following: # ----------------------------------------------------------------- # - Discord is testing our bot to make sure we fail invalid signatures # - An unauthorized request is being made to our bot from somewhere on the internet response.status_code = 401 response.body = "Bad Signature"
Now hit "Deploy", head over to Discord, and try it out!
Have more questions? Other examples you want to see? Let us know on (where else?) our Discord!
Happy building 🛰️