npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

better-discord-slash

v1.0.2

Published

Object oriented slash command Discord bot system.

Downloads

3

Readme

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.