Skip to content

Developing a Discord Bot on AWS Lambda and Python

Notifications You must be signed in to change notification settings

ker0olos/aws-lambda-discord-bot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 

Repository files navigation

Previously, this wasn't possible.

But in 2020, Discord introduced two new features: Slash Commands and Interactions Endpoints.

Here's a very straightforward guide to getting you started.


Slash Commands

(This is what Slash Commands look like)

1. Setting up the Bot and the First Command

If you don't have one already, go to Discord's Developer Portal and create a new app.


Use this URL after repleacing {APP_ID} with your own and use it to invite the bot into your server.

https://discord.com/oauth2/authorize?client_id={APP_ID}&scope=applications.commands


Now, you need to register a new slash command to test the bot.

Discord can't automatticly figure out what commands you app has, so you need to tell it each command available.

There is no UI for that, it's currently only possible via a HTTP endpoint.

import requests

APP_ID = "APP_ID"
SERVER_ID = "SERVER_ID"
BOT_TOKEN = "BOT_TOKEN"

# global commands are cached and only update every hour
# url = f'https://discord.com/api/v10/applications/{APP_ID}/commands'

# while server commands update instantly
# they're much better for testing
url = f'https://discord.com/api/v10/applications/{APP_ID}/guilds/{SERVER_ID}/commands'

json = [
  {
    'name': 'bleb',
    'description': 'Test command.',
    'options': []
  }
]

response = requests.put(url, headers={
  'Authorization': f'Bot {BOT_TOKEN}'
}, json=json)

print(response.json())

Note This is a script you keep to yourself and run locally every time you want to create a new command or update your existing ones.


  • APP_ID is visible through General Information in your app dev portal.
  • BOT_TOKEN is in Bot
  • To get your SERVER_ID, go to your discord client and enable Developer Mode in the Advanced Settings, then go to the server and right-click on the sever's name in the top-left. And now should see a new entry in the context menu called Copy Server ID.

If you are using server commands (You should for testing commands and for private bots). before running the script, make sure that the bot was alreay added to your server.

You should now be able to go the the server type / in the chat and see the command you' created, if not then something is wrong, scroll up amd try again until you can.

Commands can accept string and number inputs, But to learn more about setting up commands, check the offical docs or search for a more specific guide. https://discord.com/developers/docs/interactions/application-commands#application-command-object

2. The AWS Lambda Function

Go to your console and Create Function.

I'm using Python for this guide, but you can use whatever you like if you know how to do the same things the following script does in your language.

Now Add trigger and select API Gateway (The settings won't matter you can go with the defaults).

Look for the API endpoint. It should look like this https://a7ar46xyz.execute-api.eu-west-3.amazonaws.com/default/my-function

Copied it, go back to General Information in your discord dev portal. Find Interactions Endpoint URL and paste the endpoint there. Click Save... and you will get an error.

There are steps necessary for discord to accept the endpoint.

  1. Your endpoint must be prepared to ACK a PING message
  2. Your endpoint must be set up to properly handle signature headers

You should be familier with deploying Lambda functions. if not go read Deploy Python Lambda functions with .zip file archives. Specifically Deployment package with dependencies

You need a security library for the handle signature headers part.

Following the guild. Run pip install --target ./package pynacl

And finally, here the acutual lambda function.

import json

from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError

PUBLIC_KEY = 'YOUR_APP_PUBLIC_KEY_HERE'

def lambda_handler(event, context):
  try:
    body = json.loads(event['body'])
        
    signature = event['headers']['x-signature-ed25519']
    timestamp = event['headers']['x-signature-timestamp']

    # validate the interaction

    verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))

    message = timestamp + event['body']
    
    try:
      verify_key.verify(message.encode(), signature=bytes.fromhex(signature))
    except BadSignatureError:
      return {
        'statusCode': 401,
        'body': json.dumps('invalid request signature')
      }
    
    # handle the interaction

    t = body['type']

    if t == 1:
      return {
        'statusCode': 200,
        'body': json.dumps({
          'type': 1
        })
      }
    elif t == 2:
      return command_handler(body)
    else:
      return {
        'statusCode': 400,
        'body': json.dumps('unhandled request type')
      }
  except:
    raise

def command_handler(body):
  command = body['data']['name']

  if command == 'bleb':
    return {
      'statusCode': 200,
      'headers' : {'Content-Type': 'application/json'},
      'body': json.dumps({
        'type': 4,
        'data': {
          'content': 'Hello, World.',
        }
      })
    }
  else:
    return {
      'statusCode': 400,
      'body': json.dumps('unhandled command')
    }

This handles discord security requirements for you. But if you wan't to understand it more:

  1. Discord's Security And Authorization
  2. Gerald McAlister's Building a Serverless Discord Bot

Replace YOUR_APP_PUBLIC_KEY_HERE with your app public key also visible through General Information

Then deploy the script to Lambda (don't forget about packing the security library)


Go back to the dev portal and try to save the endpoint again, This time it should save without any errors.

  • If it saves correctly, go to your discord server and type /bleb in chat. It should work and respond with Hello, World..
  • If not then something is wrong, check the steps again until you get it working.

To add more command to the lambda function edit command_handler(body).


You must respond to any request within 3 seconds (There’s no way to increase this time). If your request will take longer than 3 seconds (i.e. an LLM query), you will need to acknowledge the user's interaction.

One method of acknowledging a request is to send back a message to user (like "Loading..") and edit the response later. Discord makes this possible by assigning an Interactions ID and an Interactions token to each interaction that expires after 15 minutes. We will be using this to our advantage.

Here is a function to immediately send something back to the user

def send(message, id, token):
    url = f"https://discord.com/api/interactions/{id}/{token}/callback"

    callback_data = {
        "type": 4,
        "data": {
            "content": message
        }
    }
    response = requests.post(url, json=callback_data)

In your command_handler, use the function as such.

message = "Loading.." # Loading message that you'd want to send back to the user.
send(message, body['id'], body['token'])

To update the message after sending out an acknowledgement, use the update function below

def update(message, token, app_id):
    url = f"https://discord.com/api/webhooks/{app_id}/{token}/messages/@original"

    # JSON data to send with the request
    data = {
        "content": message
    }

    # Send the PATCH request
    response = requests.patch(url, json=data)

In your command_handler, use the function as such

updated_message = "..."
DISCORD_BOT_ID = "...." # Please set your discord bot ID as an environmental variable or manually

update(updated_message, body['token'], DISCORD_BOT_ID)