Nacho Portion Monitor

    @greencoast/discord.js-extended
    TypeScript icon, indicating that this package has built-in type declarations

    2.2.3 • Public • Published

    ci-build-status issues bundle-size node-version version djs-version downloads-week downloads-total

    Discord.js - Extended

    discord.js-extended is a library that facilitates the repetitive tasks when creating Discord bots with Discord.js. Used by Greencoast Studios.

    Heavily inspired by discord.js-commando, it includes similar design decisions, however this adds certain functionality that isn't available like a configuration provider and automatic presence management. This package does not include all functionality provided by Commando, in fact, Commando is way more powerful than this library. Nevertheless, this was built to make it easier for us to quickly build bots without having to repeat ourselves that much.

    This was made for Discord.js v13.5, however any v13 should work fine.

    Usage

    You can visit the documentation site to see what this library offers, check the example bot, or check the bots using this section to see even more examples.

    Installation

    This package does not install Discord.js, so you should install it yourself.

    You can install this package with:

    npm install discord.js @greencoast/discord.js-extended
    

    Configuring your Client

    This package covers client configuration from environment variables and/or JSON files through the ConfigProvider class, which makes it easy to add configuration to a bot. Consider checking the documentation page to see how to use this.

    You may also specify custom validators for even more control of how config is provided to your bot. Simply, pass a customValidators property to the ConfigProvider options, and map the key of the config to its validator function. Your validator function should throw a TypeError if the given value is invalid based on your criteria.

    An example of a bot's configuration may be as follows:

    const path = require('path');
    const { ExtendedClient, ConfigProvider } = require('@greencoast/discord.js-extended');
    const { Intents } = require('discord.js');
    
    const config = new ConfigProvider({
      env: process.env, // This adds the environment variables to the config.
      configPath: path.join(__dirname, './config/settings.json'), // This is the location of the JSON file that includes the config.
      default: {
        PREFIX: '!', // Adds a default value for the PREFIX config.
        TOKEN: null, // Adds a default value for the TOKEN config.
        MY_ID: 123,
        OPTIONAL_FLAG: false,
        MY_ENUM: 'enum1'
      },
      types: { // These are the types of the configuration. The provider validates that the config receives the proper configuration types.
        PREFIX: 'string',
        TOKEN: ['string', 'null'], // With a 'null' type, you can pass 'null' to have it as null.
        MY_ID: 'number',
        OPTIONAL_FLAG: ['boolean', 'null'],
        MY_ENUM: 'string',
        MY_NUM_ARRAY: 'number[]' // You can pass arrays through JSON or comma-separated values through env variables.
      },
      customValidators: { // These are the custom validators to use instead of the basic type based validator.
        MY_ENUM: (value) => {
          const validValues = ['enum1', 'enum2', 'enum3'];
          if (!validValues.includes(value)) {
            throw new TypeError(`${value} is not a valid value for MY_ENUM, you should use: ${validValues.join(', ')}`);
          }
        }
      }
    });
    
    const client = new ExtendedClient({
      config,
      intents: [Intents.FLAGS.GUILDS]
    });

    Make sure to check the documentation page to see the structure of the JSON file required and the type of environment variables that are supported.

    Creating a Client

    This package exports an extension of the regular Discord.js Client that contains all the extended functionality. You can create one by using the following:

    const path = require('path');
    const { ExtendedClient, ConfigProvider } = require('@greencoast/discord.js-extended');
    const { Intents } = require('discord.js');
    
    const config = new ConfigProvider({
      env: process.env,
      configPath: path.join(__dirname, './config/settings.json'),
      default: {
        KEY: 'value'
      }
    });
    
    const client = new ExtendedClient({
      config, // The config provider instance used by the bot.
      debug: true, // Enable debug-mode.
      owner: '123', // The ID of the bot's owner.
      prefix: '!', // The bot's prefix to be used.
      presence: {
        templates: ['presence 1', 'presence 2', '{custom_key} hi!'], // The presence statuses used by this bot.
        refreshInterval: 3600000, // Update the bot's presence every hour.
        customGetters: {
          custom_key: async() => Math.random().toString() // Define custom getters for keys to replace on the presence strings.
        }
      },
      errorOwnerReporting: true, // Sends DMs to the bot's owner whenever a command throws an error.
      intents: [Intents.FLAGS.GUILDS]
    });
    
    client.login(<YOUR_DISCORD_TOKEN_HERE>);

    The presence option in the client's constructor allows you to configure the presence statuses to be used by the bots. These presence statuses may include information from the bot, such as: number of guilds connected to, number of commands, the time the bot went online, or even custom data... Consider checking the documentation page to see what information you can include in your presence statuses and how to add custom getters for your own presence messages.

    Adding defaults to your Client

    You can register default handlers for the Discord.js Client events, as well as the ExtendedClient custom events.

    client.registerDefaultEvents().registerExtraDefaultEvents();

    The package also comes with default commands that you can use on your bot. Check the Default Commands to see the available commands you can register right away. You can register them like:

    client.registry.registerDefaults();

    Adding a Database Provider to your Client

    The package comes with persistent data functionality that can be enabled. The DataProvider is an abstract class that serves as common interface to use. By default, data is stored by guild, but you can also store/get data on a global scope.

    You can use the following methods:

    await client.dataProvider.get(guild, 'key'); // Get a value for 'key' in guild.
    await client.dataProvider.set(guild, 'key', 'value'); // Set 'value' for 'key' in guild.
    await client.dataProvider.delete(guild, 'key'); // Delete a key-value pair for 'key' in guild.
    await client.dataProvider.clear(guild); // Clear all data in a guild.
    
    await client.dataProvider.getGlobal('key'); // Get a value for 'key' in the global scope.
    await client.dataProvider.setGlobal('key', 'value'); // Set 'value' for 'key' in the global scope.
    await client.dataProvider.deleteGlobal('key'); // Delete a key-value pair for 'key' in the global scope.
    await client.dataProvider.clearGlobal(); // Clear all data in the global scope.

    You need to set up a data provider before being able to use any of these methods.

    LevelDataProvider

    LevelDataProvider is a data provider implemented with a LevelDB backend. In order to use this, you need to install level. This data provider was made for level@7.0.1 but any v7 should work.

    npm install level
    

    In order to set up this data provider, you need to import it and add it to your client.

    const LevelDataProvider = require('@greencoast/discord.js-extended/dist/providers/LevelDataProvider').default;
    
    const provider = new LevelDataProvider(client, 'database_location');
    
    client.on('ready', async() => {
      await client.setProvider(provider);
    });

    RedisDataProvider

    RedisDataProvider is a data provider implemented with a Redis backend. In order to use this, you need to install redis. This data provider was made for redis@4.0.2 but any v4 should work.

    npm install redis
    

    In order to set up this data provider, you need to import it and add it to your client.

    const RedisDataProvider = require('@greencoast/discord.js-extended/dist/providers/RedisDataProvider').default;
    
    const provider = new RedisDataProvider(client, { 
      url: 'redis://alice:foobared@awesome.redis.server:6380'
    });
    
    client.on('ready', async() => {
      await client.setProvider(provider);
    });

    Keep in mind that Redis was originally meant to be a cache, data is deleted by default across service restarts. You can achieve persistence by properly configuring your Redis instance.

    Localizing Your Bot

    The package contains a Localizer class to help with the localization of your bot. In order to use it, you should pass a localizer object to your client constructor and initialize the localizer in the ready event. Keep in mind that you absolutely need the GUILDS intent in your client for this to work properly.

    const client = new ExtendedClient({
      localizer: {
        defaultLocale: 'en', // The default locale for your bot.
        dataProviderKey: 'locale', // The key to be used to store the locale for each guild in the client's data provider.
        localeStrings: locales
      },
      intents: ['GUILDS']
    });
    
    client.on('ready', async() => {
      await client.setDataProvider(new DataProvider()); // Should set the data provider before.
      await client.localizer.init(); // Initializes the localizer.  
    });

    The localeStrings object should map the name of the locale to another object that maps the message keys with their corresponding message string in its respective language. In the example above, the variable locales could be:

    const locales = {
      en: {
        'message.test.hello': 'Hello',
        'message.test.bye': 'Bye',
        'message.test.with_value': 'Hello {name}!'
      },
      es: {
        'message.test.hello': 'Hola',
        'message.test.bye': 'Adios',
        'message.test.with_value': 'Hola {name}!'
      },
      fr: {
        'message.test.hello': 'Bonjour',
        'message.test.bye': 'Au revoir',
        'message.test.with_value': 'Bonjour {name}!'
      }
    };

    Locale messages should follow the ICU format.

    In case a message is not available in a certain locale and it is requested, the message from the default locale will be picked.

    Inside a command, you may use the localizer in the following manner:

    class MyCommand extends SlashCommand {
      async run(interaction) {
        const localizer = this.client.localizer.getLocalizer(interaction.guild);
        
        interaction.reply(localizer.t('message.test.hello'));
        interaction.reply(localizer.t('message.test.with_value', { name: 'your name' }));
      }
    }

    This uses the locale saved for the guild. You can change the locale for the guild as such:

    class MyCommand extends SlashCommand {
      async run(interaction) {
        const localizer = this.client.localizer.getLocalizer(interaction.guild);
        
        await localizer.updateLocale('fr');
        interaction.reply(`Updated locale to ${localizer.locale}`);
      }
    }

    If you're outside the context of a guild, you can still use the localizer by using:

    client.localizer.t('message.test.with_value', 'es', { name: 'your name' });

    Creating Commands

    The package contains a RegularCommand to facilitate the creation of commands. In order to create one, you need to create a class that extends RegularCommand and implements a run() method.

    const { Permissions } = require('discord.js');
    const { RegularCommand } = require('@greencoast/discord.js-extended');
    
    module.exports = class MyCommand extends RegularCommand {
      constructor(client) {
        super(client, {
          name: 'cmd', // The command's name. In this case, users need to write !cmd for the command to work.,
          aliases: ['mycmd', 'alias2'], // The command's aliases. With this, users can write !mycmd and !alias2 for the command to work.
          description: 'My command.', // The command's description.
          group: 'my_group', // The ID of the group that holds this command.
          emoji: ':robot:', // The emoji that represents this command. This is used by the default HelpCommand. Defaults to ':robot:'.
          guildOnly: true, // Whether the command may only be used in a guild. Defaults to false.
          nsfw: false, // Whether the command may only be used in a NSFW channel. Defaults to false.
          ownerOnly: false, // Whether the command may only be used by the owner. Defaults to false.
          userPermissions: Permissions.FLAGS.MANAGE_CHANNELS, // The PermissionResolvable representing the permissions that users require to execute this command. Defaults to null.
          ownerOverride: true, // Whether the owner may execute this command even if they don't have the required permissions. Defaults to true.
        });
    
        run(message, args) {
          message.reply('Hi!');
        }
      }
    }

    This command should be saved in a folder with the ID of its group. These folders should be contained in a bigger folder. The folder tree should look like this:

    .
    ├── commands
    │   ├── my_group
    │   |   ├── MyCommand.js
    │   |   ├── AnotherCommand.js
    │   ├── other_group
    │   |   ├── OtherCommand.js
    

    Once you have this folder structure, you can register your commands in the client:

    client.registry
      .registerGroups([
        ['my_group', 'My Group'],
        ['other_group', 'Other Group']
      ])
      .registerCommandsIn('./commands/folder/location');

    If you don't register the groups before-hand, the commands will not be registered.

    Creating Slash Commands

    You can also use Slash Commands, the package contains a SlashCommand to facilitate the creation of slash commands. In order to create one, you need to create a class that extends SlashCommand and implements a run() method.

    const { Permissions } = require('discord.js');
    const { SlashCommand } = require('@greencoast/discord.js-extended');
    const { SlashCommandBuilder } = require('@discordjs/builders');
    
    module.exports = class MyCommand extends SlashCommand {
      constructor(client) {
        super(client, {
          name: 'cmd', // The command's name. In this case, users need to write !cmd for the command to work.,
          aliases: ['mycmd', 'alias2'], // The command's aliases. With this, users can write !mycmd and !alias2 for the command to work.
          description: 'My command.', // The command's description.
          group: 'my_group', // The ID of the group that holds this command.
          emoji: ':robot:', // The emoji that represents this command. This is used by the default HelpCommand. Defaults to ':robot:'.
          guildOnly: true, // Whether the command may only be used in a guild. Defaults to false.
          nsfw: false, // Whether the command may only be used in a NSFW channel. Defaults to false.
          ownerOnly: false, // Whether the command may only be used by the owner. Defaults to false.
          userPermissions: Permissions.FLAGS.MANAGE_CHANNELS, // The PermissionResolvable representing the permissions that users require to execute this command. Defaults to null.
          ownerOverride: true, // Whether the owner may execute this command even if they don't have the required permissions. Defaults to true.
          dataBuilder: new SlashCommandBuilder() // You do not need to use .setName() and .setDescription(), they're handled internally with the data above.
        });
    
        run(interaction) {
          interaction.reply('Hi!');
        }
      }
    }

    To use slash commands, you need to install the following package to access the SlashCommandBuilder class:

    npm install @discordjs/builders
    

    Keep in mind that you do not need to use SlashCommandBuilder.setName() and SlashCommandBuilder.setDescription() as these are already set by the command constructor.

    Deploying Slash Commands

    Slash commands require a special procedure to deploy them and have them live on Discord.

    Development

    For development, it is recommended to have a testing server for the development of the bot. With this in mind, you should set testingGuildID on your client options and have the following ready event handler.

    client.on('ready', async() => {
      client.deployer.rest.setToken(config.get('TOKEN'));
      await client.deployer.deployToTestingGuild();
    });

    This will update the slash commands ONLY on your testing server.

    Production

    For production, you should have a command deploy script, that could be run by a CI pipeline on a new version of your bot. The following example can work as a potential deploy script.

    require('dotenv').config();
    const path = require('path');
    const { ExtendedClient, ConfigProvider } = require('@greencoast/discord.js-extended');
    
    const config = new ConfigProvider({
      env: process.env,
      configPath: path.join(__dirname, './config/settings.json'),
      types: {
        TOKEN: 'string'
      }
    });
    
    const client = new ExtendedClient({
      config,
      intents: []
    });
    
    client.registry
      .registerDefaults()
      .registerGroups([
        ['util', 'Utility'],
        ['slash', 'Slash Commands']
      ])
      .registerCommandsIn(path.join(__dirname, './commands'));
    
    client.on('ready', async() => {
      try {
        client.deployer.rest.setToken(config.get('TOKEN'));
        await client.deployer.deployGlobally();
      } catch (error) {
        console.error('Something happened!', error);
        process.exit(1);
      }
    });
    
    client.on('commandsDeployed', (commands) => {
      console.log(`Successfully deployed ${commands.length} commands globally!`);
      process.exit(0);
    });
    
    client.login(client.config.get('TOKEN'));

    Keep in mind that you only need to deploy globally once. Also, this process can take up to an hour to be reflected on Discord. You should not use client.deployer.deployGlobally() for development.

    Inviting Your Bot

    Inviting your bot requires you to build a specific invite link. Head over to the Discord Applications Page and go into your bot's page. Under the OAuth2 tab, head over to the OAuth2 URL Generator and select (at least) the scopes bot and application.commands. At the bottom, another box should show up where you should pick the corresponding permissions your bot requires to function properly. Once you have all that set, an invite URL will be generated. You should invite the bot to your server (or any server) with this link.

    Testing

    You can run the unit tests for this package by:

    1. Cloning the repo:
    git clone https://github.com/greencoast-studios/discord.js-extended
    
    1. Installing the dependencies.
    npm install
    
    1. Running the tests.
    npm test
    

    Bots Using This

    Here's a list of some bots that use this library.

    Authors

    This library was made by Greencoast Studios.

    Install

    npm i @greencoast/discord.js-extended

    DownloadsWeekly Downloads

    58

    Version

    2.2.3

    License

    MIT

    Unpacked Size

    353 kB

    Total Files

    168

    Last publish

    Collaborators

    • moonstar-x