
Setup a Discord Bot
how-toon 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:
- Most Discord bots run on servers, whereas Merrymake is serverless.
- 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-interactionsDiscord 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.tsif (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.
- They are unusual HTTP headers. The usual HTTP headers are not accessible.
- This handler is the first in the trace. Later services do not have access to these headers.
Then we'll parse the payload:
app.tsconst { 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.tsif (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.tsthrow `unknown interaction type: ${type}`;Now we are ready to register the bot:
Deploy the service
$ mm deployPut the public key in an envvar
$ mm envvar new PUBLIC_KEY secretCreate an api-key
$ mm key new DiscordRegister 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.tsif (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.tsimport { 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.tsexport 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 deployCompile the code
$ tscRegister the commands
$ DISCORD_TOKEN=[your bot token] APP_ID=[your application id] node commandsApart 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.
