Setup a Discord Bot

how-to

by Christian Clausen

on Dec 3, 2024 (created Nov 23, 2024) • 5 min

This guide shows how to adapt and deploy the Discord example bot to run in Merrymake. We assume you have a basic understanding of Merrymake already. If this is not the case, please complete one of the tutorials first.

Running a Discord bot on the Merrymake platform presents two unique challenges, which we'll solve in this guide:

  1. Most Discord bots run on servers, whereas Merrymake is serverless.
  2. We need to meet both Merrymake's and Discord's security requirements.

Preparation

First, you'll need to create an app in the Discord developer portal if you don't have one already.

We'll need three important keys from the developer portal. They are confidential, so store them somewhere safe while we are setting up the bot.

Application ID
Found under General Information.
Public Key
Found under General Information.
Token
Under Bot click Reset Token.

Installation

Click on Installation in the left sidebar, in this guide we focus on server installations, so we only need the Guild Install selected.

On the Installation page in the Default Install Settings section, under Guild Install, add the bot scope. When we select bot, a new Permissions menu appears to select the bot user's permissions. In this guide we need Send Messages.

To install your app to your test server, copy the default Install Link for your app from the Installation page. Paste the link in your browser and hit enter, then select Add to server in the installation prompt.

We select our test server, and follow the installation prompt. Once we add our app to our test server, we see our bot appear in the member list.

Implement the Bot

We can implement our bot in any language. This guide assumes you start from a new repository based on Merrymake's basic Typescript template.

First, we install the Discord api:

$ npm install discord-interactions

Discord requires an extra layer of security, on top of Merrymake's. In order to pass the Discord validation we have to reject a bad request. We start our handler with the security check:

app.ts
if (envelope.headers === undefined) return; const isValid = await verifyKey( payloadBuffer, envelope.headers["x-signature-ed25519"], envelope.headers["x-signature-timestamp"], process.env.PUBLIC_KEY ); if (!isValid) { replyToOrigin({ content: "Invalid signature", "status-code": 401, }); return; }

Notice: The code above relies on two headers x-signature-ed25519 and x-signature-timestamp. These two headers are available only because they meet two requirements.

  1. They are unusual HTTP headers. The usual HTTP headers are not accessible.
  2. This handler is the first in the trace. Later services do not have access to these headers.

Then we'll parse the payload:

app.ts
const { type, data } = JSON.parse(payloadBuffer.toString());

The second rule of Discord's bot validation process is that our bot responds to a PING with a PONG. We implement this here:

app.ts
if (type === InteractionType.PING) { replyToOrigin({ content: { type: InteractionResponseType.PONG } }); return; }

Our handler can now pass the validation process of a Discord bot. To make errors easier to spot we finish the code with a:

app.ts
throw `unknown interaction type: ${type}`;

Now we are ready to register the bot:

Deploy the service

$ mm deploy

Put the public key in an envvar

$ mm envvar new PUBLIC_KEY secret

Create an api-key

$ mm key new Discord

Register the endpoint

In the Discord developer portal, in the pane General Information, under Interactions Endpoint URL put: https://rapids.merrymake.io/[api-key]/[event]

Add a /Test Command

With the bot installed we are ready to add a command. Continuing the handler from above, we can add some code to handle a /test command after the PING:

app.ts
if (type === InteractionType.APPLICATION_COMMAND) { const { name } = data; // "test" command if (name === "test") { // Send a message into the channel where command was triggered from return replyToOrigin({ content: { type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: { content: `👋🌍` }, }, }); } }

We also need to register our commands, so we create a second file with this content:

command.ts
import { InstallGlobalCommands } from "./utils.js"; // Simple test command const TEST_COMMAND = { name: "test", description: "Basic command", type: 1, integration_types: [0, 1], contexts: [0, 1, 2], }; const ALL_COMMANDS = [TEST_COMMAND]; InstallGlobalCommands(process.env.APP_ID!, ALL_COMMANDS);

We also add a utils.ts file, with a couple of useful functions:

utils.ts
export interface Command { name: string; description: string; type: number; integration_types: number[]; contexts: number[]; } export async function DiscordRequest( endpoint: string, options: { method: "PUT" | "DELETE" | "PATCH"; body?: any } ) { // append endpoint to root API URL const url = "https://discord.com/api/v10/" + endpoint; // Stringify payloads if (options.body) options.body = JSON.stringify(options.body); // Use 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": "DiscordBot (https://github.com/discord/discord-example-app, 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; } export async function InstallGlobalCommands( appId: string, commands: Command[] ) { // 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: commands }); } catch (err) { console.error(err); } }

To make the command available we need to:

Deploy the service

$ mm deploy

Compile the code

$ tsc

Register the commands

$ DISCORD_TOKEN=[your bot token] APP_ID=[your application id] node commands

Apart from the utils file, you'll need to repeat all the steps in this section every time you want to add new commands to your bot.

Christian Clausen
Nov 23, 2024