Skip to content

Build a Discord Bot to Track the ISS with Napkin

Use Discord's webhook-based API to build a custom slash command

Post written by Nick Sypteras

Nick Sypteras

@NickSypteras
Build a Discord Bot to Track the ISS with Napkin

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.

ISS Bot Demo

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.

Register Slash Command

Discord Bot Code

(Bonus) ISS Map Function

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

Settings > Bot
, create a new one, and enable the Message Content Intent (seen at the bottom).

Bot Configuration

Lastly, copy the bot's Token by clicking the

Copy
button. We'll need it for the next step.

Bot Token Copy Button

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, response
import os
import uuid
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError

OK, now the verification part which trips a lot of people (maybe just me) up. We'll use the

nacl
module to verify the contents of the the request header sent from Discord. If it's invalid, we respond with a
401
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>/information
PUBLIC_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-authorization
verify_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

type
field. According to the docs, if we receive a request where
type
equals 1, it's a PING request, and we should respond with a simple JSON object containing
{"type": 1}
to acknowledge it.

# https://discord.com/developers/docs/interactions/receiving-and-responding
if 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

General Information > Interactions Endpoint URL
. Save changes and Discord will test our bot to make sure it verifies signatures correctly. You should see a confirmation message.

Bot Verified Confirmation

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

401
.

Successful PING RequestSuccesful PING request

Failed PING RequestFailed 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

iss-map
. 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, response
import os
import uuid
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
# you can find your application public key at
# https://discord.com/developers/applications/<my_app_id>/information
PUBLIC_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-authorization
verify_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!

ISS Bot Demo

Have more questions? Other examples you want to see? Let us know on (where else?) our Discord!

Happy building 🛰️

Subscribe to the Napkin Newsletter

Get tips, news and advice from the backend community. Learn new things to hone your backend skills right in your inbox!

Write and deploy cloud functions from your browser.

Napkin is the quickest way to build your backend