1.0.2 • Published 2 years ago

better-discord-slash v1.0.2

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

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.

OptionTypeDefault valueDescription
homeGuildIdstringundefinedGuild ID used when setting the permissions of global commands. If not supplied, global command permissions will not be acknowledged.
defaultResponseInteractionResponse = string \| Discord.MessagePayload \| Discord.InteractionReplyOptionsundefinedThe default message the bot replies to a slash command interaction with whenever a command does not return a message nor replies during execution.
errorResponseInteractionResponse = string \| Discord.MessagePayload \| Discord.InteractionReplyOptionsundefinedThe default message the bot replies to a slash command interaction with whenever an error occurs during execution of a command handler.
defaultUpdateInteractionResponse = string \| Discord.MessagePayload \| Discord.InteractionReplyOptionsundefinedThe 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.
includeTypeScriptbooleantrueWhether to include TypeScript source files when loading handler files. Useful for those running through ts-node.
watchCommandsbooleantrueWhether to watch command source files for updates to therefore reload them when updates are made.
dynamicComponentCustomIdSplitterstring":"The character used to separate the command name / ID from the custom ID of a component handled within a command handler.
logSettingsnumber0A 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

OptionBitDescription
INTERACTION_RECIEVE0b0000_0000_0001, 1 << 0, 0x001, 1Announce the collection of an interaction
COMMAND_RECIEVE0b0000_0000_0010, 1 << 1, 0x002, 2Announce the identification of an interaction as a command
COMPONENT_RECIEVE0b0000_0000_0100, 1 << 2, 0x004, 4Announce the identification of an interaction as a component
DYNAMIC_COMPONENT0b0000_0000_1000, 1 << 3, 0x008, 8Announce the identification of an interaction as a dynamically handled component
COMMAND_HANDLE0b0000_0001_0000, 1 << 4, 0x010, 16Supply information about the handling of commands
COMPONENT_HANDLE0b0000_0010_0000, 1 << 5, 0x020, 32Supply information about the handling of components
COMMAND_LOAD0b0000_0100_0000, 1 << 6, 0x040, 64Announce the loading of new commands, or reloading of existing commands
COMMAND_REGISTER0b0000_1000_0000, 1 << 7, 0x080, 128Announce the registration of new and / or existing commands
COMMAND_PERMISSIONS0b0001_0000_0000, 1 << 8, 0x100, 256Announce the successful change in permissions for a previously loaded command
EVENT_LOAD0b0010_0000_0000, 1 << 9, 0x200, 512Announce the loading of new event handlers, or reloading of existing event handlers
EVENT_START0b0100_0000_0000, 1 << 10, 0x400, 1024Announce the starting of event handlers; communicating their availability of use
COMPONENT_LOAD0b1000_0000_0000, 1 << 11, 0x800, 2048Announce 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.