Skip to content

An OOPy starting point for discord.js bot development using slash commands

Notifications You must be signed in to change notification settings

ChasJC23/DiscordSlash

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Discord.js object oriented template

This TypeScript template, inspired by the recommended JavaScript template given by discord.js, is an entirely object oriented, dynamically loaded system for slash command bots. This template is entirely generic, meaning if there are data sources you need that the template doesn't supply by default, some extra class inheritance may be used to easily add extra data sources.

The basics

The bot class

First of all, let's create a bot and load in our commands from a local folder:

import { REST } from "@discordjs/rest";
import * as Discord from "discord.js";
import { DiscordBot } from "./discord";
import * as path from "path";
import { token, clientId } from "./config.json";

const botClient = new Discord.Client({ intents: [Discord.Intents.FLAGS.GUILDS] });
const rest = new REST({ version: '9' }).setToken(token);

const options = { homeGuildId: "1234567890" };
const myBot = new DiscordBot(botClient, rest, clientId, options);

botClient.login(token)
.then(async () => {
  await myBot.loadCommands(path.join(__dirname, "commands"));
  myBot.beginAwaitingInteractions();
});

Command loading automatically registers all commands found in the path given, applying their permissions. Subdirectories may be used for command organisation.

Bot constructor options

There are many settings we can make use of to control how the bot will work. These settings are supplied through the options argument.

Option Type Default value Description
homeGuildId string undefined Guild ID used when setting the permissions of global commands. If not supplied, global command permissions will not be acknowledged.
defaultResponse InteractionResponse = string | Discord.MessagePayload | Discord.InteractionReplyOptions undefined The default message the bot replies to a slash command interaction with whenever a command does not return a message nor replies during execution.
errorResponse InteractionResponse = string | Discord.MessagePayload | Discord.InteractionReplyOptions undefined The default message the bot replies to a slash command interaction with whenever an error occurs during execution of a command handler.
defaultUpdate InteractionResponse = string | Discord.MessagePayload | Discord.InteractionReplyOptions undefined The default change to a message made when an interaction from a message component is received and the handler does not return an update. Updates during execution are not detected.
includeTypeScript boolean true Whether to include TypeScript source files when loading handler files. Useful for those running through ts-node.
watchCommands boolean true Whether to watch command source files for updates to therefore reload them when updates are made.
dynamicComponentCustomIdSplitter string ":" The character used to separate the command name / ID from the custom ID of a component handled within a command handler.
logSettings number 0 A bitmask describing whether to log certain aspects of the bots execution. This has no effect on warnings or errors raised by the bot.

Bot logging settings

Option Bit Description
INTERACTION_RECIEVE 0b0000_0000_0001, 1 << 0, 0x001, 1 Announce the collection of an interaction
COMMAND_RECIEVE 0b0000_0000_0010, 1 << 1, 0x002, 2 Announce the identification of an interaction as a command
COMPONENT_RECIEVE 0b0000_0000_0100, 1 << 2, 0x004, 4 Announce the identification of an interaction as a component
DYNAMIC_COMPONENT 0b0000_0000_1000, 1 << 3, 0x008, 8 Announce the identification of an interaction as a dynamically handled component
COMMAND_HANDLE 0b0000_0001_0000, 1 << 4, 0x010, 16 Supply information about the handling of commands
COMPONENT_HANDLE 0b0000_0010_0000, 1 << 5, 0x020, 32 Supply information about the handling of components
COMMAND_LOAD 0b0000_0100_0000, 1 << 6, 0x040, 64 Announce the loading of new commands, or reloading of existing commands
COMMAND_REGISTER 0b0000_1000_0000, 1 << 7, 0x080, 128 Announce the registration of new and / or existing commands
COMMAND_PERMISSIONS 0b0001_0000_0000, 1 << 8, 0x100, 256 Announce the successful change in permissions for a previously loaded command
EVENT_LOAD 0b0010_0000_0000, 1 << 9, 0x200, 512 Announce the loading of new event handlers, or reloading of existing event handlers
EVENT_START 0b0100_0000_0000, 1 << 10, 0x400, 1024 Announce the starting of event handlers; communicating their availability of use
COMPONENT_LOAD 0b1000_0000_0000, 1 << 11, 0x800, 2048 Announce the loading of new component handlers, or reloading of existing component handlers

Basics of creating commands

In order to create a command, you must create a class that extends from either the GlobalCommandHandler or GuildCommandHander class in some way. This must then be set as the default export of the file:

export default class FooCommand extends GlobalCommandHandler

This newly extended class must then give values to the following attributes, and override the following methods, in some way:

export default class FooCommand extends GlobalCommandHandler {
  public readonly longRunning: boolean = false; // a value indicating whether the execution time of the command is expected to exceed 3 seconds
  public readonly permissions?: Discord.ApplicationCommandPermissionData[] = []; // the permissions for the command
  public readonly slashData: Discord.SlashCommandBuilder = new Discord.SlashCommandBuilder(); // the underlying data sent to Discord during registration.
  protected ftn(int: Discord.CommandInteraction): HandlerReturn { 
    // the code run when the command interaction is received
  }
}

The Discord client is set as an attribute of the command class on loading, and will therefore be accessible in command execution through this.discordClient.

When creating a guild command, you must also give a value to the guildId attribute, as this indicates which guild, or guilds, the command is available to.

export default class BarCommand extends GuildCommandHandler {
  // ...
  public readonly guildId: string | string[];
  // ...
}

Basics of creating event handlers

The process for creating event handlers is very similar in nature. You create a class that extends from the generic EventHandler class. This newly created class must then be set as the default export of the file with the following attributes assigned to:

export default class JoinGuildEventHandler extends EventHandler<"guildCreate"> {
  public readonly eventType = "guildCreate"; // must be set to the value given in the type argument
  public readonly oneTime: boolean = false; // determines whether this event handler is run using .on or .once
  protected ftn(newGuild: Discord.Guild): void | PromiseLike<void> {
    // the arguments this function receives are reliant on the event type.
  }
}

Basics of handling message components

There are two ways of handling message components. You can either write a handler like you would a command or event by creating a class extending from the ButtonHandler or SelectMenuHandler class, depending on the component you wish to handle. You create the component as you usually would, giving it a custom ID. If the bot receives an interaction with that custom ID, it sends the interaction to the associating handler you've created.

export default class PingBtnHandler extends ButtonHandler {
  public readonly customId: string = "ping"; // the custom ID of the component to handle
  protected override ftn(int: Discord.ButtonInteraction): HandlerReturn {
    // the code run when an interaction of this type is received
  }
}

The other option, which may be cleaner and more useful at times, is to have the component be handled within a command class. In order to achieve this, the custom ID of the component sent must be in the format <command name | command id>:<custom id>. Note: the character used to separate the command name from the custom name, by default :, may be changed using the dynamicComponentCustomIdSplitter setting. A good character choice would be one not available for use in command names as the code for distinguishing the command name / id from the custom id only considers the first instance of the separator character found.

export default class FooCommand extends GlobalCommandHandler {
  // ...
  public readonly slashData = new BetterSlashCommandBuilder()
    .setDefaultPerms(false)
    .setName("foo")
    .setDescription("example command")
  
  protected override ftn(int: Discord.CommandInteraction): HandlerReturn { 
    const yesBtn = new Discord.MessageButton({ customId: "foo:Yes", style: "SUCCESS", label: "yes" });
    const noBtn = new Discord.MessageButton({ customId: "foo:No", style: "DANGER", label: "no" });
    const infoBtn = new Discord.MessageButton({ customId: "foo:Info", style: "LINK", label: "info", url: "https://discord.com" });
    return { components: new Discord.MessageActionRow({ components: [ yesBtn, noBtn, infoBtn ] }) }
  }

  protected override handleButton(int: Discord.ButtonInteraction): HandlerReturn {
      // runs if a button interaction is received and is not handled elsewhere
  }

  private handleButtonYes(int: Discord.ButtonInteraction): HandlerReturn {
    // runs if a button with custom ID foo:Yes is received
  }

  private handleButtonNo(int: Discord.ButtonInteraction): HandlerReturn {
    // runs if a button with custom ID foo:No is received
  }

  protected override handleSelectMenu(int: Discord.SelectMenuInteraction, ... args: any[]): HandlerReturn {
    // runs if a select menu is received and is not handled elsewhere
  }
}

The power of generics

The bot class, written out fully, is a massive mess of generics:

class DiscordBot<GlobalT extends GlobalCommandHandler, GuildT extends GuildCommandHandler, EventT extends EventHandler<any>, ButtonT extends ButtonHandler, SelectMenuT extends SelectMenuHandler>;

The reason for all these generics is to allow for handlers to have greater access to data, either supplied through their constructors or in some cases, as an argument given into the method responsible for handling the received interaction.

Extra constructor arguments

If you want to include additonal arguments to the constructors of your handlers, you should create classes that extend them like so:

abstract class MyGlobalCommandHandler extends GlobalCommandHandler {
  myAttr: any;
  constructor(discordClient: Discord.Client, myAttr: any) {
    super(discordClient);
    this.myAttr = myAttr;
  }
}

To be complete, you'll need to extend GlobalCommandHandler, GuildCommandHandler, EventHandler<K extends keyof Discord.ClientEvents>, ButtonHandler, & SelectMenuHandler. The necessary code to give them extra constructor arguments is identical. The discordClient must be the first argument in the constructor. In order to make the bot make use of these additional arguments for the constructor, you can supply them to the loadCommands, loadEvents and loadComponents methods where appropriate, or extend the bot yourself:

class MyBot extends DiscordBot<MyGlobalCommandHandler, MyGuildCommandHandler, MyEventHandler<any>, MyButtonHandler, MySelectMenuHandler> {
  myAttr: any;
  constructor(discordClient: Discord.Client, rest: REST, clientId: string, myAttr: any, options: DiscordBotOptions) {
    super(discordClient, rest, clientId, options);
    this.myAttr = myAttr;
  }

  public override loadCommands(commandPath: string) {
    super.loadCommands(commandPath, this.myAttr);
  }

  public override loadEvents(eventPath: string) {
    super.loadEvents(eventPath, this.myAttr);
  }

  public override loadComponents(componentPath: string) {
    super.loadComponents(componentPath, this.myAttr)
  }
}

Extra handler arguments

If instead you wish to have extra data be inputted to the method used for the handler's execution, you can extend the appropriate classes in a similar way

abstract class MyGlobalCommandHandler extends GlobalCommandHandler {
  protected abstract override ftn(int: Discord.CommandInteraction, myArg: any);
}

And of course, make the bot use them:

class MyBot extends DiscordBot<MyGlobalCommandHandler, MyGuildCommandHandler, EventHandler<any>, MyButtonHandler, MySelectMenuHandler> {
  myAttr: any;
  constructor(discordClient: Discord.Client, rest: REST, clientId: string, myAttr: any, options: DiscordBotOptions) {
    super(discordClient, rest, clientId, options);
    this.myAttr = myAttr;
  }
  public override beginAwaitingInteractions() {
    super.beginAwaitingInteractions(this.myAttr);
  }
}

This approach is not supported for event handlers. As the methods you override in these examples all have an ... args: any[] argument by default; it is, although recommended, not necessary to extend these classes in some cases.

About

An OOPy starting point for discord.js bot development using slash commands

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published