YosefA

30th December 2023

Building A Discord Bot With Interactions

Yosef Alnajjar Full Stack Engineer

Node

TypeScript

Discord

Bot

Back-End

Post Cover

In this post I’ll walk you through how I built my discord bot, this should give you a jump start into building your own bot and build on top of it to add more functionality if you’d like. Here is the link to the Github repo of the project as you may prefer to follow the full code while reading this post.

To get started we’ll need to create a discord app in the discord developer portal. The bot I built listens to commands or interactions and responds with a random quote, I called it WiseMan. When you create the bot in the portal, you’ll need to keep these two values to authenticate requests from the discord API to your bot

Application in discord protal

Now we’ll need to build the app, you can choose the backend technology you are most comfortable with for this one I’ll use ExpressJS for our bot’s API and we’ll use TypeScript because I love types!

Creating the project

If you have project already running or know how to create a starter project with Express and TypeScript then you can skip this step

Let’s initialise the project by creating a new folder and initialise an npm package

mdkir wise-man-bot && cd wise-man-bot && pnpm init -y

Next up we’ll need to install express and dotenv (for accessing our env variables)

pnpm express dotenv

Now let’s add typescript to the project

pnpm i -D typescript @types/express @types/node

We’ll need to generate tsconfig file as well

npx tsc --init

In the tsconfig specify a dist folder as our output for the typescript complier

{
  "compilerOptions": {
    ...
    "outDir": "./dist"
    ...
  }
}

The project structure

Let’s define the project files structure, create the following src folder with these files/

/src
- commands.ts // Used for keeping the slash commands and registering them
- index.ts // The main file which will serve the endpoints of our server
- utils.ts // Used for the discord util functions

In commnds.ts we’ll add our slash commands, the slash command consists of a name and description of the command, there are other properties and options that you can specify but for simplicity we’ll just define it with a name and description

const GET_QUOTE_COMMAND: Command = {
  name: 'quote',
  description: 'Get a random quote from the wise man',
}

We defined our first slash command to get the a quote from the bot, I’m using custom types for now but if you want more official types from discord that are currently in progress check out discord-api-types

Next up we’ll need to register our slash command and to do that we’ll need to make a call to the discord API so let’s write down the function that will do discord requests

// utils.ts
export const DiscordRequest = async (
  endpoint: string,
  options: DiscordRequestOptions,
): Promise<globalThis.Response> => {
  // append endpoint to root API URL
  const url = 'https://discord.com/api/v10/' + endpoint
  // Use node-fetch to make requests
  const res = await fetch(url, {
    headers: {
      Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
      'Content-Type': 'application/json; charset=UTF-8',
      'User-Agent':
        'WiseManBot (https://github.com/yosefanajjar/wise-man-bot, 1.0.0)',
    },
    ...options,
  })
  // throw API errors
  if (!res.ok) {
    const data = await res.json()
    console.log(res.status)
    throw new Error(JSON.stringify(data))
  }
  // return original response
  return res
}

The function above will take a discord api endpoint and the options which will contain the api call method and the body containing the data we want to send I’m using node fetch as per the example in the discord docs but you can use something else like axios I don’t think that should be a problem.

Notice for interacting with the discord api we’ll need your discord app’s token which you can get the value for from the developer portal, copy the token and add it to your .env

Bot Token Generation

Your .env file should look something like this

APP_ID=
DISCORD_TOKEN=
PUBLIC_KEY=

I’ve added the function below to make the API call to register/install the slash commands, now we can call this function in our commands.ts file

// utils.ts
export const InstallGlobalCommands = async (
  appId: string | undefined,
  commands: Command[],
): Promise<void> => {
  // API endpoint to overwrite global commands
  const endpoint = `applications/${appId}/commands`

  try {
    // This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
    await DiscordRequest(endpoint, {
      method: 'PUT',
      body: JSON.stringify(commands), // Stringify payloads
    })
  } catch (err) {
    console.error(err)
  }
}
// commands.ts
import 'dotenv/config'
import { InstallGlobalCommands } from './utils.js'
import { type Command } from './types/index.js'

const GET_QUOTE_COMMAND: Command = {
  name: 'quote',
  description: 'Get a random quote from the wise man',
}

export const ALL_COMMANDS: Command[] = [GET_QUOTE_COMMAND]

void (async () => {
  await InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS)
})()

To run this command let’s add a register script to our package.json to run this file. I’m using tsx to run the typscripts file in real time, it’s a modern version of ts-node

// package.json
"scripts": {
	"register": "tsx src/commands.ts",
	...
}

Now that we’ve got our command register it’s time to write the endpoint that will receive the interaction from the discord api and respond to it. But before adding the endpoint we need to make sure that we verify the requests are coming from the discord api and for that we’ll use the discord-interactions package for verification and to handle signature headers

// utils.ts
export function VerifyDiscordRequest(clientKey: string) {
  return function (req: Request, res: Response, buf: Buffer, encoding: string) {
    const signature = req.get('X-Signature-Ed25519') as Signature
    const timestamp = req.get('X-Signature-Timestamp') as Signature

    const isValidRequest = verifyKey(buf, signature, timestamp, clientKey)
    if (!isValidRequest) {
      res.status(401).send('Bad request signature')
      throw new Error('Bad request signature')
    }
  }
}

The function above uses the verifyKey function from discord-interactions to verify the api request, we’ll use this function in a middleware that will run before every request. It’s important to place this middleware before any body parsing that you do with packages like body-parser because we want to pass the function above the raw body

// Parse request body and verifies incoming requests using discord-interactions package
app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) }))

After adding the middleware we can define the interactions endpoint that discord will send requests to, the endpoint must be prepared to acknowledge a PING message and handle the slash commands you’ve registered

app.post(
  '/interactions',
  async (req, res): Promise<InteractionsApiResponse | void> => {
    // Interaction type and data
    const { type, data } = req.body

    /**
     * Handle verification requests
     */
    if (type === InteractionType.PING) {
      return res.send({ type: InteractionResponseType.PONG })
    }

    /**
     * Handle slash command requests
     * See https://discord.com/developers/docs/interactions/application-commands#slash-commands
     */
    if (type === InteractionType.APPLICATION_COMMAND) {
      const { name } = data

      if (name === 'quote') {
        const quote = await quoteOfTheDay() // makes an api call to get random quote
        // Send a message into the channel where command was triggered from
        return res.send({
          type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
          data: {
            content: quote,
          },
        })
      }
    }
  },
)

Deploying your API

Time to deploy the server, but before deploying let’s make sure we’ve got all the needed scripts in our package.json

"scripts": {
    "build": "npx tsc", // compiles ts code and outputs to /dist
    "start": "node dist/index.js", // starts local server of compiled code
    "register": "tsx src/commands.ts", // registers the slash commands
    "dev": "nodemon - exec 'tsx' src/index.ts" // runs local dev server
  },

We’ll need to deploy the app to get a url of our interactions endpoint to give to the discord app in the developer portal, you can deploy to test things quickly using a tool like ngrock I’ll deploy the api on Render they provide an interface that makes it easy to deploy an API

Connect Render with your Github repo and update the settings for the build and run commands since we’re using typescript for our project. You’ll also need to provide values for the environment variables we’ve added under the Environment tab

Render Settings

Once we deploy successfully and get a URL let’s go add it to our discord bot

Discord Interactions Url

Save your changes and to go test the app we need to generate a url to add the bot to a server where we can test the commands, go to the OAuth2 tab under URL Generator we need to specify the permissions needed for our bot to respond to slash commands. Select the permissions below and add the bot to the server of your choice

Bot Generate URL

After adding the bot to a server you’ll see the slash commands our bot can execute and running it should give us a random quote as we planned

Bot Slash Command

Bot Slash Command Execution

Common Issues

  • You might run into issues when running the slash commands if your bot was inactive for more than 30 minutes and that’s due to cold starts and the discord api has a timeout so it’s likely to fail. The free plan on Render will make your API go to sleep if you are not using it, the solution to that is to upgrade your plan or you can use another provider like AWS Bot Slash Command Failure
  • Signature size error: you’ll get this error if you are not verifying the discord calls correctly, make sure that your middleware is receiving the raw body and test the api endpoint by executing the command in the discord app, as in Postman you won’t have the correct values for the signature headers

Newsletter

Leave your email to subscribe to my monthly newsletter where I share updates on events and blog posts I write 🚀 where I keep your in the loop of what goes in my life and the tech world from my window 🪟