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

discord.ts-architecture

v2.3.0

Published

A typescript library for better discord bots

Downloads

94

Readme

discord.ts-architecture

GitHub License NPM Version GitHub Actions Workflow Status NPM Downloads

A Typescript library for better Discord bots. This library provides an OOP approach to the Discord.js library for Typescript projects. It does not "simplify" the approach advertised by the framework, but transforms the crude approach into a stable architecture ready for larger projects.

This is not a complete architecture, and only covers interaction handling, interaction response, and Discord login, but it does provide extended abstract classes for all interaction types with built-in features like automatic deferral.

Wiki

See the Wiki for a detailed documentation of the library.

Example Code

See the Example Code for a complete example of a Discord bot using this library.

Examples

Initial Setup

Your index.ts could look like this

import { InteractionHandler, DiscordHandler, Logger } from '../lib';
import { Partials, GatewayIntentBits, Events } from 'discord.js';
import PingCommand from './commands/PingCommand';
import PingButton from './buttons/PingButton';
import PingRoleSelectMenu from './selectMenus/PingRoleSelectMenu';
import PingStringSelectMenu from './selectMenus/PingStringSelectMenu';

// this should be in some sort of env file and not checked into any repository
// you get this from the discord developer portal
const DISCORD_TOKEN = 'YOUR_DISCORD_TOKEN';
const DISCORD_CLIENT_ID = 'YOUR_DISCORD_CLIENT_ID';

// we create a new instance of the DiscordHandler
const discordHandler = new DiscordHandler(
  // and pass the Client Partials
  // this Partial for example allows to receive uncache messages
  [Partials.Message],
  // and the Gateway Intents
  // we want to receive events from Guilds, GuildMessages and GuildMembers
  [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers]
);

// lets create our commands
const pingButton = new PingButton();
const pingRoleSelectMenu = new PingRoleSelectMenu();
const pingStringSelectMenu = new PingStringSelectMenu();
const pingCommand = new PingCommand(pingButton, pingRoleSelectMenu, pingStringSelectMenu);

// now it is time to create a new instance of the InteractionHandler
// and pass the command, button and select menu interaction models we want to use
const interactionHandler = new InteractionHandler(
  [pingCommand],
  [pingButton],
  [pingRoleSelectMenu, pingStringSelectMenu]
);

// we activate the interactionCreate event for all interaction models
interactionHandler.activateInteractionCreate(discordHandler);

// and listen for the ready event of the discord client
discordHandler.once(Events.ClientReady, (readyClient) => {
  Logger.info('Discord client is ready and logged in as ' + readyClient.user?.tag);
});

// now login to discord
discordHandler.login(DISCORD_TOKEN).then(async () => {
  Logger.info('Logged into Discord');
  // after logging in, we can register the interactions with each guild
  // this is done by calling the init method of the interactionHandler
  await interactionHandler.init(DISCORD_TOKEN, DISCORD_CLIENT_ID, discordHandler);
  Logger.info('Initialized interactions');
});

Yes, this is a lot. But that's it. There is no further configuration required.

  • Your commands are already pushed to the guilds.
  • Your interactions are already handled
  • Exceptions caught and logged
  • You have an automatic reply deferral if the interaction takes too long

Creating custom commands

A custom command is represented as a class derived from the CommandInteractionModel, and might look like this (for a ping command).

import {
  ChatInputCommandInteraction,
  SlashCommandStringOption,
  ButtonBuilder,
  ButtonStyle,
  StringSelectMenuBuilder,
  ActionRowBuilder,
  RoleSelectMenuBuilder
} from 'discord.js';
import { CommandInteractionModel, MessageHandler } from 'discord.ts-architecture';
import PingButton from '../buttons/PingButton';
import PingRoleSelectMenu from '../selectMenus/PingRoleSelectMenu';
import PingStringSelectMenu from '../selectMenus/PingStringSelectMenu';

// we extend the CommandInteractionModel to create a new command
export default class PingCommand extends CommandInteractionModel {
  private pingButton: PingButton;
  private pingRoleSelectMenu: PingRoleSelectMenu;
  private pingStringSelectMenu: PingStringSelectMenu;

  // we add some other interaction models used in this command
  public constructor(
    pingButton: PingButton,
    pingRoleSelectMenu: PingRoleSelectMenu,
    pingStringSelectMenu: PingStringSelectMenu
  ) {
    // load the super constructor with all needed information
    super(
      'ping', // The command used in Discord
      'Pings the bot', // The description of the command (not more then 120 characters)
      [
        new SlashCommandStringOption()
          .setName('additional message')
          .setDescription('An additional message to send with the ping')
          .setRequired(false)
      ], // The SlashComandOptions used in this command
      2000, // The amount of milliseconds to defer the reply if no reply was already made. If undefined, does not defer reply
      true, // If true, will defer reply as ephemeral, making the reply ephemeral aswell
      ['admin'] // The roles that are allowed to use the command
    );
    this.pingButton = pingButton;
    this.pingRoleSelectMenu = pingRoleSelectMenu;
    this.pingStringSelectMenu = pingStringSelectMenu;
  }

  // and override the handle method to implement the command
  public override async handle(interaction: ChatInputCommandInteraction): Promise<void> {
    // activate the deferred reply, so if this takes to long, the interaction will be deferred
    this.activateDeferredReply(interaction);
    // check if the user is allowed to use this command
    const allowed = await this.checkPermissions(interaction);
    // if not, we send an error message
    if (allowed == false) {
      // using the MessageHandler to send a reply an ephemeral error
      await MessageHandler.replyError({
        interaction: interaction, // the interaction to reply to
        title: 'No permission', // the title of the reply
        description: 'You are not allowed to use this command', // the description of the reply
        color: 0xff0000, // the color of the reply, if not set, will be 0xff0000 by default
        components: [
          // and lets create some components
          // first a button to retry the command
          // we use the component of the pingButton and set the style to danger
          new ActionRowBuilder<ButtonBuilder>().addComponents([this.pingButton.component.setStyle(ButtonStyle.Danger)]),
          // and a select menu to select an option
          // lets try role select menu
          new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents([this.pingRoleSelectMenu.component])
        ]
      });
      return;
    }

    // the user is allowed
    // so lets get the additional message from the command options
    const additionalMessage = interaction.options.getString('additional message');
    // if the additional message is set, we add it to the reply
    const message = additionalMessage ? `Pong! ${additionalMessage}` : 'Pong!';

    // and we send the reply, this is not ephemeral by default
    await MessageHandler.reply({
      interaction: interaction, // the interaction to reply to
      title: message, // the title of the reply
      components: [
        // and lets create the same components as above
        new ActionRowBuilder<ButtonBuilder>().addComponents([this.pingButton.component]),
        // and try string select menus
        new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([this.pingStringSelectMenu.component])
      ]
    });
  }
}

Again, this is a lot for a simple ping command. But it does give you a lot of extra functionality.

  • Your options are registered with the command as you specify them.
  • If your command takes longer than 2 seconds, the interaction is deferred and then properly handled by the MessageHandler.
  • Your InteractionHandle is exception catched, preventing the bot from shutting down.
  • In this example, we didn't respond with a text message, we responded with an embed message containing categories and buttons.

Final Usage Advice

The initial setup for this is higher for very simple projects. I suggest creating a template for Discord bot projects. There is nothing else to do besides the setup shown. Creating additional commands and button interactions all follow the same architectural structure. This makes for better readability, maintainability, and more robust behaviour. In addition, it is recommended to still catch every exception and missed rejection for an application.