Build a Discord Bot With AWS Lambda + API Gateway | by jakjus | Apr, 2022

Photo by Alexander Kovacs on Unsplash

Recently, Discord has introduced Interactions and Slash Commands to ease users’ experience with bots. Partially, the reason was to secure users’ privacy — bots are now slightly forced to abandon flags for reading messages in Discord Servers — which was previously a point of concern regarding illegal data processing by bot developers.

These are the most known features, but on top of that, the update has added a possibility to interact with mentioned Interactions through the Interactions Endpoint URL.

Developer Dashboard’s Field for Interactions Endpoint URL.

This allows us to move from a traditional listener app hosted on a server to a serverless app *applause*that runs a Lambda Function on each Interaction invocation.

During the coding, we will jump between “AWS”, “Code” and “Discord Developer Portal”.

AWS

We will use AWS API Gateway and AWS Lambda for our task. It’s a base toolset for serverless architecture on AWS. Functions will be written in Node.js.

  1. Create an account and log in at https://aws.amazon.com
  2. Go to API Lambda service by using top-bar navigation
  3. Choose “Functions” section in the sidebar (if not yet chosen)
  4. Click “Create function”
AWS Lambda Main Page.

If you were brave enough to click it right away with plugged in credit card — you’ve probably used AWS or AWS Lambda before.

If you hesitated — it’s a moment to calm you down: The AWS Lambda free tier includes one million free requests per month and 400,000 GB-seconds of compute time per month. Way more than you will need for the sake of this tutorial and probably ever after (though I wish you exceeding the limit!).

5. Choose your Function name, pick Node.js 14.x and click “Create function”

Create function panel.

After creation, you should see the dashboard below.

Initial Lambda dashboard. Yeah, we will be clicking like that. Not a point in time to implement IaC.

Great! We have our lambda with a boilerplate code. Now add an API Trigger — we need some URI to pass to Discord Portal, right?

Trigger configuration.

Enable CORS to allow Discord’s host origin. Some day in the future, you may want to change the openness of API endpoint and CORS allowed domains so that you do not get bot-spammed (it’s not common for AWS resources though).

The result:

Trigger created.

Go to the API endpoint by clicking on its URL: you should see “Hello from Lambda!”

Code

Create project directory serverless_discord/. Inside, create a subdirectory lambda_bot/ and index.js within. Use the following code:

In the code above, we follow the structure that you can already see in AWS Lambda boilerplate code (exports.handler function).

Next, we fulfill endpoint requirements (payload verification and ping reply) set by Discord in the documentation. Verification is done with tweetnacl package, that we do not have yet. Let’s install it!

Best case would be if your node --version is close to the Lambda runtime’s version (we chose 14.x ) — ideally, if it’s the same. If it’s too far apart — along with npm — the packed modules may not work in Lambda.

npm i tweetnacl

Good. Now, let’s zip all of that (with node_modules!) and put back into our Lambda. While still being inside of your directory, type:

zip -r ../lambda_bot.zip *

AWS

Upload the zip in Lambda dashboard — section “Code”.

Lambda dashboard.

or through CLI instead, if you have AWS CLI installed and configured:

aws lambda update-function-code 
--function-name discord
--zip-file fileb://../lambda_bot.zip

Everything looks great. Quick app creation at Discord in 3, 2, 1, go!

Discord Developer Portal

Go to Discord Developer Portal, choose New Application and pick a name. Copy previously created API endpoint to Interactions Endpoint URL field in bot’s configuration below.

My real AWS API Gateway URL.

and click Save!

Saving performs Discord-side check of their requirements — they send a set of POST requests to your endpoint.

Scroll to the top of the page for notification and — there you have it! Big Green Success bar — your Bot is live! You made it!

Huh? What? You’ve got some error? Umm, what’s the error message?

You sent me this unhelpful screenshot.

How can I fix the endpoint, when all I know is that “something” is wrong? At most, we may assume, that one of their two requirements is not fulfilled.

AWS

To debug it from AWS side, you can make some test snippets and run them against your code. Unfortunately, it’s not easy to find an exact call example from Discord to mock this.

You can also debug it quickly by running the code on your own computer (just strip the code in index.js from handler function), but you then you’d need external domain redirecting to your local IP, which has its own risks.

Let’s add console logs to have any idea, if the request is coming and if we interpret it correctly.

Add logging in very first lines of index.js

const nacl = require('tweetnacl');exports.handler = async (event) => {
console.log(event)
// Checking signature (requirement 1.)
...

and click “Deploy”.

In Discord Developer Portal, “Save” Discord Application settings (as a reminder — it will rerun Discord’s POST request to our Lambda). With the event (hopefully) logged, check it in CloudWatch.

Lambda dashboard — navigating to CloudWatch.

I found this error message:

Error message.

(the event itself was logged in one field higher, which may be helpful)

The error points to Line 15 in index.js which states:

Buffer.from(PUBLIC_KEY, 'hex')

and points rightfully so. We did not pass PUBLIC_KEY environment variable, which is assigned to variable PUBLIC_KEY beforehand.

const PUBLIC_KEY = process.env.PUBLIC_KEY;

Get the Public Key of your app from Discord Developer Portal and insert in AWS Lambda’s: Configuration → Environment Variables → Edit… → Add… (do NOT use quotation marks for env variables)

Key: PUBLIC_KEY, Value: 55ff1c234...

Run “Save Changes” now!

All your edits have been carefully recorded.

At the end of index.js add handler for a command, that we will soon register.

I want a Slash Command /foo that replies “bar” (ok, not too mind-blowing).

Copy and paste the updated code to AWS Lambda code editor or zip it and upload it like the last time. Click “Deploy.”

Registering /foo command

Create a second subdirectory in serverless_discord/ called register_commands/ and navigate into it.

➜  serverless_discord mkdir register_commands && cd register_commands

Create a new file register.js with:

To add missing modules, run:

npm i axios dotenv

But, why do we have two different packages (folders) now?

Well, this one will not be on AWS Lambda and does not have to be anywhere remote, actually. Ideally, registering commands, should happen during CI/CD Pipeline, when there are commands changes. Without CI/CD, it’s totally OK to run the script manually from your local PC after you develop something new.

Create .env file and change values ​​to yours:

ForGUILD_ID, go to Discord App, create a Discord Guild (colloquially Discord Server). In your User Settings → Advanced → Developer Mode ON. Go back to main discord screen, right click on your Guild and click Copy ID.

Then, go to Discord Developer Portal, choose your app → Bot → Add Bot → Reset Token → Copy — it is your BOT_TOKEN.

Find APP_ID in General Information section.

Go to OAuth2 section → URL Generator. Choose Scopes: bot, application.commands. Bot permissions: Use Slash Commands.

Copy URL generated below and visit it — choose Guild, that you have just made.

Generated URL.

You are right, we limit the scope to just one guild (yours). Slash commands will NOT appear in other guilds, that you will invite bot into. When creating Slash Commands with Guild Scope, it applies changes immediately — that’s why it is always used in the development process until the bot is very ready. Creating or changing Global Commands is rolled out for each guild in 0–60 minutes.

Changing scope to Global later on, should take you around 5 minutes with the official documentation.

Enough talking, let’s deploy the command!

➜  register_commands node register.js➜  register_commands

No error means success! Go quickly to your Discord Guild chat, start typing /foo and press Enter.

Drumroll…
BAR!

Congratulations! 🏆

Your serverless bot is now u-p- um_a_n_d_ _r_u_n_n_i_n_g_ waiting for events 🙂

Upsides:

  1. Low cost
  2. Easy to implement
  3. Very well scalable — no need for sharding (with only linear cost increase!)

Downsides:

  1. Locked to AWS (vendor-lock) (this particular solution)
  2. May require CI/CD implementation more than “serverful” options — otherwise, altering AWS resources may be tedious
  3. This architecture needs proper logging and debug messages on client and server-side in order to be effective.

In this article you have learned:

  1. Basic serverless toolkit on AWS
  2. The current state of Discord Bot programming
  3. Challenges with AWS Lambda — development, deployment, debugging
  4. Payload verification with nacl and making requests with axios

Additional sources:

As your newly made function says, it’s time to celebrate at the bar!

(Now the article intro picture doesn’t feel so odd, right?)

Leave a Comment