When setting up an Application on Discord, you have the option to receive standard events from the client through webhooks. Discord will make a request to a pre-defined HTTPS endpoint and provide event details within a JSON payload.
- Uses the Discord Interactions API
- Uses Cloudflare Workers for hosting
- Interactions Responses
- Basic Interactions Responses
- Deferred Interaction Responses
- Update Deferred Interaction Responses
- Messages contents & options:
- Embeds
- Components
- Attach Files
- Flags, and many more...
- Visit the Discord Developer Portal and log in to your account
- If you want it now, copy the
APPLICATION ID
andPUBLIC KEY
. We'll be storing it in the secrets later with dotenv and/or worker environment variables.
- Grab the token for your bot, and keep it somewhere safe locally or in the secrets.
- Enable all the privileged gateway intents.
- Select
bot
andapplications.commands
scopes. - Select all text permissions or select the permissions that you consider necessary for your bot.
- Copy the generated URL, and paste it into the browser and select the server where you'd like to develop your bot.
- Make sure to install the Wrangler CLI.
- To install Wrangler, ensure you have Node.js, npm and/or pnpm installed.
- I'm using pnpm so I'll run
$ pnpm i -g wrangler
. - Move to your preferred directory and then run
$ pnpm create cloudflare
to create a new cloudflare application (it will create a directory for your application). You will probably need to authorize your cloudflare account before continue. - Set a name for your application and select "Hello World" script.
- It will ask you if you want to use typescript, I selected no. Then, your worker is created, select yes to deploy.
- Select your cloudflare account to deploy.
- When it's succesful deployed, visit the Cloudflare Dashboard
- Click on the
Workers & Pages
tab. - Click on your worker.
- You will see your worker route.
- If you access your worker route it will show a "Hello World" message. That means the worker script is deployed.
- Now go back and open your application folder on your code editor. There you have the "Hello World" worker script.
- We don't need these files so we will remove them and add the templates files to your application folder.
- Make sure to set the correct name and main worker router path to
src/index.js
on yourwrangler.toml
.
- Make sure to perform a
$ pnpm install
to install all dependencies.
- You'll need to store your discord bot secrets:
APPLICATION ID
,PUBLIC KEY
andTOKEN
in your.env
file. - Dotenv is a critical requirement for the script responsible for registering bot commands when executing locally
register.js
.
- Those secrets can only be used by the worker and not locally.
- Visit the Cloudflare Dashboard and go to your worker.
- Click on the Settings -> Variables tab, add your secrets and save.
Now that we have our template and secrets added we can register our commands by running locally register.js
.
The code responsible for registering our commands can be found in the file register.js
. These commands can be registered either globally, enabling their availability across all servers where the bot is installed, or they can be specifically registered for a single server. For the purpose of this example, our emphasis will be placed on global commands.
/**
* Register slash commands with a local run
*/
import { REST, Routes } from "discord.js";
import * as commands from "./commands.js";
import "dotenv/config";
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
const commandsArray = Object.values(commands);
try {
console.log("Started refreshing application (/) commands.");
await rest.put(Routes.applicationCommands(process.env.DISCORD_APPLICATION_ID), { body: commandsArray });
console.log("Successfully reloaded application (/) commands.");
} catch (error) {
console.error(error);
}
To register your commands run:
$ pnpm discord:register
If everything is ok, it should print that the commands have been reloaded successfully.
Now, to make the commands work you have to set an INTERACTIONS ENDPOINT URL at Discord Developer Portal. This will be the url of your worker.
By setting your worker url and saving it, discord will send a PING interaction to verify your webhook.
All the API calls from Discord will be sent via a POST request to the root path ("/"). Subsequently, we will utilize the discord-interactions npm module to effectively interpret the event and transmit the outcomes. As shown in the index.js
code.
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
/**
* The `PING` message is used during the initial webhook handshake, and is
required to configure the webhook in the developer portal.
*/
console.log('Handling Ping request');
return create(request_data.type);
} else {
// ... command interactions
}
});
If everything is ok, your interactions endpoint url will be saved and your bot will respond to commands on the server it is in.
Whenever you want to deploy your worker to apply changes you must run the command:
$ pnpm worker:deploy
Bot will reply with the string the user entered.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// Reply /string command (Bot will reply with the string the user entered)
case C.STRING_COMMAND_EXAMPLE.name: {
const string = getValue("text");
return reply(`Your string: ${string}`);
}
// ... Other cases
default:
return error(400, "Unknown Type")
}
});
}
});
// ...
commands.js
export const STRING_COMMAND_EXAMPLE = {
name: "string",
description: "command description.",
options: [ // Use options if you need the user to make any input with your commands
{
"name": "text",
"description": "field description.",
"type": CommandType.STRING,
"required": true
}
]
};
// ... Other commands
Discord server
Bot will reply with a random number between 0 and 100.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// Reply /number command (Bot will reply with a random number between 0 and 100) (example command)
case C.NUMBER.name: {
const userId = member.user.id; // user who triggered command
const randomNumber = getRandom({min: 0, max: 100});
return reply(`<@${userId}>'s random number: ${randomNumber}`);
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const NUMBER = {
name: "number",
description: "Get a random number between 0 and 100.",
options: []
};
// ... Other commands
Discord server
Bot will reply with an embed example message.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// Reply /embed command (Bot will reply with an embed example message)
case C.EMBED_EXAMPLE.name: {
const message = "Bot message";
const hexcolor = "FB05EF";
const embeds = [];
embeds.push({
color: Number("0x" + hexcolor),
author: {
name: "Author name",
icon_url: ""
},
title: "Title",
url: "https://example.com",
description: "Description",
});
return reply(message, {
embeds
});
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const EMBED_EXAMPLE = {
name: "embed",
description: "command description.",
options: []
};
// ... Other commands
Discord server
Bot will reply with a button component example message.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// Reply /button command (Bot will reply with a button component example message)
case C.BUTTON_EXAMPLE.name: {
const message = "Bot message";
const button = [];
button.push({
type: MessageComponentTypes.BUTTON,
style: ButtonStyleTypes.LINK,
label: "Open Browser",
url: "https://example.com"
});
return reply(message, {
components: [{
type: MessageComponentTypes.ACTION_ROW,
components: button
}]
});
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const BUTTON_EXAMPLE = {
name: "button",
description: "command description.",
options: []
};
// ... Other commands
Discord server
For uploading files and fetching URLs, from my experience, I recommend using Deferred Messages and Worker's waitUntil().
Useful if your command needs more than 3 seconds to respond, otherwise reply() will fail.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
/**
* For uploading files and fetching URLs, from my experience, I recommend using Deferred Messages and Worker's waitUntil()
* (Useful if your command needs more than 3 seconds to respond, otherwise reply() will fail)
*/
// Defer Reply and Update /file command (Bot will fetch for a file url and then upload it and defer reply)
case C.UPLOAD_FILE_EXAMPLE.name: {
const followUpRequest = async () => {
const message = "Bot message";
const files = [];
const fileFromUrl = await fetch("https://i.kym-cdn.com/photos/images/newsfeed/001/564/945/0cd.png");
const blob = await fileFromUrl.blob();
files.push({
name: "filename.png",
file: blob
});
// Update defer
return deferUpdate(message, {
token,
application_id: env.DISCORD_APPLICATION_ID,
files
});
}
context.waitUntil(followUpRequest()); // function to followup, wait for request and update response
return deferReply(); //
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const UPLOAD_FILE_EXAMPLE = {
name: "files",
description: "command description.",
options: []
};
// ... Other commands
Discord server
You can combine all the options (embeds, components, files) according to your creativity and the needs of your command.
Bot will reply a message that contains text content, embeds, components and files.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// You can combine all the options (embeds, components, files) according to your creativity and the needs of your command
// Defer Reply and Update /combined command (Bot will reply a message that contains text content, embeds, components and files)
case C.COMBINED_OPTIONS_EXAMPLE.name: {
const followUpRequest = async () => {
const message = "Bot message";
const embeds = [];
const button = [];
const files = [];
const fileFromUrl = await fetch("https://i.kym-cdn.com/photos/images/newsfeed/001/564/945/0cd.png");
const blob = await fileFromUrl.blob();
const hexcolor = "FB05EF";
embeds.push({
color: Number("0x" + hexcolor),
author: {
name: "Author name",
icon_url: ""
},
title: "Title",
url: "https://example.com",
description: "Description",
});
files.push({
name: "filename.png",
file: blob
});
button.push({
type: MessageComponentTypes.BUTTON,
style: ButtonStyleTypes.LINK,
label: "Open Browser",
url: "https://example.com"
});
// Update defer
return deferUpdate(message, {
token,
application_id: env.DISCORD_APPLICATION_ID,
embeds,
components: [{
type: MessageComponentTypes.ACTION_ROW,
components: button
}],
files
});
}
context.waitUntil(followUpRequest()); // function to followup, wait for request and update response
return deferReply(); //
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const COMBINED_OPTIONS_EXAMPLE = {
name: "combined",
description: "combined options example.",
options: []
};
// ... Other commands
Discord server
Ship two users together, showing their love compatibility percentage and their ship name.
index.js
// ...
router.post("/", async (req, env, context) => {
const request_data = await req.json();
if (request_data.type === InteractionType.PING) {
// ... PING ...
} else {
const { type, data, member, guild_id, channel_id, token } = request_data;
const { name, options, resolved } = data;
return create(type, options, async ({ getValue = (name) => name }) => {
// Bot command cases
switch (name) {
// ... Other cases
// Extra funny command
// Reply /ship command: Ship two users together, shows their "love" compatibility percentage and their ship name on an embed.
case C.SHIP.name: {
const u1 = getValue("user1"); // First user value
const u2 = getValue("user2"); // User to ship value
const message = "";
const embeds = [];
const p = getRandom({min: 0, max: 100});
const { users } = resolved;
const chars_name1 = users[u1].username.substring(0, 3);
const chars_name2 = users[u2].username.substring(users[u2].username.length - 2);
const ship_name = chars_name1 + chars_name2;
const hexcolor = "FB05EF";
embeds.push({
color: Number("0x" + hexcolor),
description: `❤️ | <@${u1}> & <@${u2}> are **${p}%** compatible.\n❤️ | Ship name: **${ship_name}**.`
})
return reply(message, {
embeds
});
}
// ... Other cases
default:
return error(400, "Unknown Type");
}
});
}
});
// ...
commands.js
export const SHIP = {
name: "ship",
description: "Ship two users together, showing their love compatibility percentage and their ship name.",
options: [
{
"name": "user1",
"description": "First user.",
"type": CommandType.USER,
"required": true
},
{
"name": "user2",
"description": "User to ship",
"type": CommandType.USER,
"required": true
}
]
};
// ... Other commands
Discord server