Skip to content

Basic template for setting up a Discord Application that support slash commands using Cloudflare Workers

License

Notifications You must be signed in to change notification settings

ahmedrangel/discord-app-workers-template

Repository files navigation

Discord Application Slash Commands Template Using Clourflare Workers

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.

Features

  • Interactions Responses
    • Basic Interactions Responses
    • Deferred Interaction Responses
    • Update Deferred Interaction Responses
  • Messages contents & options:
    • Embeds
    • Components
    • Attach Files
    • Flags, and many more...

Setting up a Discord Application

Click New Application, and choose a name

  • If you want it now, copy the APPLICATION ID and PUBLIC KEY. We'll be storing it in the secrets later with dotenv and/or worker environment variables.

image

Click on the Bot tab, and create your bot.

  • Grab the token for your bot, and keep it somewhere safe locally or in the secrets.
  • Enable all the privileged gateway intents.

Click on the OAuth2 tab, and choose the URL Generator.

  • Select bot and applications.commands scopes.
  • Select all text permissions or select the permissions that you consider necessary for your bot.

image image

  • Copy the generated URL, and paste it into the browser and select the server where you'd like to develop your bot.

image

Creating a Cloudflare Worker Application

  • 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.

image

  • It will ask you if you want to use typescript, I selected no. Then, your worker is created, select yes to deploy.

image

  • 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.

image

  • If you access your worker route it will show a "Hello World" message. That means the worker script is deployed.

image

  • Now go back and open your application folder on your code editor. There you have the "Hello World" worker script.

image

  • 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 your wrangler.toml.

image

  • Make sure to perform a $ pnpm install to install all dependencies.

Storing secrets / environment variables

Using dotenv

  • You'll need to store your discord bot secrets: APPLICATION ID, PUBLIC KEY and TOKEN in your .env file.
  • Dotenv is a critical requirement for the script responsible for registering bot commands when executing locally register.js.

image

Using Cloudflare Worker Environment Variables

  • 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.

image

Next Steps

Register commands

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.

image

Set Interactions Endpoint URL on your Discord Application

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.

image

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.

Deploy worker

Whenever you want to deploy your worker to apply changes you must run the command:

$ pnpm worker:deploy

Basic Command Examples

/string

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

image

/number

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

image

/embed

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

image

/button

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

image

/files

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

image

/combined

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

image

/ship

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

image

Releases

No releases published

Packages

No packages published