Home

Building a Discord Bot

Create an application on Discord Developer Portal#

  1. Go to https://discord.com/developers/applications (login using your discord account if required).
  2. Click on New Application button available at left side of your profile picture.
  3. Name your application and click on Create.
  4. Go to Bot section, click on Add Bot, and finally on Yes, do it! to confirm.

That's it. A new application is created which will hold our Slash Command. Don't close the tab as we need information from this application page throughout our development.

Before we can write some code, we need to curl a discord endpoint to register a Slash Command in our app.

Fill DISCORD_BOT_TOKEN with the token available in the Bot section and CLIENT_ID with the ID available on the General Information section of the page and run the command on your terminal.


_10
BOT_TOKEN='replace_me_with_bot_token'
_10
CLIENT_ID='replace_me_with_client_id'
_10
curl -X POST \
_10
-H 'Content-Type: application/json' \
_10
-H "Authorization: Bot $BOT_TOKEN" \
_10
-d '{"name":"hello","description":"Greet a person","options":[{"name":"name","description":"The name of the person","type":3,"required":true}]}' \
_10
"https://discord.com/api/v8/applications/$CLIENT_ID/commands"

This will register a Slash Command named hello that accepts a parameter named name of type string.

Code#

index.ts

_94
// Sift is a small routing library that abstracts away details like starting a
_94
// listener on a port, and provides a simple function (serve) that has an API
_94
// to invoke a function for a specific path.
_94
import { json, serve, validateRequest } from 'sift'
_94
// TweetNaCl is a cryptography library that we use to verify requests
_94
// from Discord.
_94
import nacl from 'tweetnacl'
_94
_94
enum DiscordCommandType {
_94
Ping = 1,
_94
ApplicationCommand = 2,
_94
}
_94
_94
// For all requests to "/" endpoint, we want to invoke home() handler.
_94
serve({
_94
'/discord-bot': home,
_94
})
_94
_94
// The main logic of the Discord Slash Command is defined in this function.
_94
async function home(request: Request) {
_94
// validateRequest() ensures that a request is of POST method and
_94
// has the following headers.
_94
const { error } = await validateRequest(request, {
_94
POST: {
_94
headers: ['X-Signature-Ed25519', 'X-Signature-Timestamp'],
_94
},
_94
})
_94
if (error) {
_94
return json({ error: error.message }, { status: error.status })
_94
}
_94
_94
// verifySignature() verifies if the request is coming from Discord.
_94
// When the request's signature is not valid, we return a 401 and this is
_94
// important as Discord sends invalid requests to test our verification.
_94
const { valid, body } = await verifySignature(request)
_94
if (!valid) {
_94
return json(
_94
{ error: 'Invalid request' },
_94
{
_94
status: 401,
_94
}
_94
)
_94
}
_94
_94
const { type = 0, data = { options: [] } } = JSON.parse(body)
_94
// Discord performs Ping interactions to test our application.
_94
// Type 1 in a request implies a Ping interaction.
_94
if (type === DiscordCommandType.Ping) {
_94
return json({
_94
type: 1, // Type 1 in a response is a Pong interaction response type.
_94
})
_94
}
_94
_94
// Type 2 in a request is an ApplicationCommand interaction.
_94
// It implies that a user has issued a command.
_94
if (type === DiscordCommandType.ApplicationCommand) {
_94
const { value } = data.options.find(
_94
(option: { name: string; value: string }) => option.name === 'name'
_94
)
_94
return json({
_94
// Type 4 responds with the below message retaining the user's
_94
// input at the top.
_94
type: 4,
_94
data: {
_94
content: `Hello, ${value}!`,
_94
},
_94
})
_94
}
_94
_94
// We will return a bad request error as a valid Discord request
_94
// shouldn't reach here.
_94
return json({ error: 'bad request' }, { status: 400 })
_94
}
_94
_94
/** Verify whether the request is coming from Discord. */
_94
async function verifySignature(request: Request): Promise<{ valid: boolean; body: string }> {
_94
const PUBLIC_KEY = Deno.env.get('DISCORD_PUBLIC_KEY')!
_94
// Discord sends these headers with every request.
_94
const signature = request.headers.get('X-Signature-Ed25519')!
_94
const timestamp = request.headers.get('X-Signature-Timestamp')!
_94
const body = await request.text()
_94
const valid = nacl.sign.detached.verify(
_94
new TextEncoder().encode(timestamp + body),
_94
hexToUint8Array(signature),
_94
hexToUint8Array(PUBLIC_KEY)
_94
)
_94
_94
return { valid, body }
_94
}
_94
_94
/** Converts a hexadecimal string to Uint8Array. */
_94
function hexToUint8Array(hex: string) {
_94
return new Uint8Array(hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16)))
_94
}

Deploy the Slash Command Handler#


_10
supabase functions deploy discord-bot --no-verify-jwt
_10
supabase secrets set DISCORD_PUBLIC_KEY=your_public_key

Navigate to your Function details in the Supabase Dashboard to get your Endpoint URL.

Configure Discord application to use our URL as interactions endpoint URL#

  1. Go back to your application (Greeter) page on Discord Developer Portal
  2. Fill INTERACTIONS ENDPOINT URL field with the URL and click on Save Changes.

The application is now ready. Let's proceed to the next section to install it.

Install the Slash Command on your Discord server#

So to use the hello Slash Command, we need to install our Greeter application on our Discord server. Here are the steps:

  1. Go to OAuth2 section of the Discord application page on Discord Developer Portal
  2. Select applications.commands scope and click on the Copy button below.
  3. Now paste and visit the URL on your browser. Select your server and click on Authorize.

Open Discord, type /Promise and press Enter.

Run locally#


_10
supabase functions serve discord-bot --no-verify-jwt --env-file ./supabase/.env.local
_10
ngrok http 54321