1.10.0 • Published 2 years ago

discord-botly v1.10.0

Weekly downloads
-
License
GPL-3.0-or-later
Repository
github
Last release
2 years ago

Discord Botly

Discord Botly is a Discord Bot framework that wraps the Discord.js library.

NPM

Table of Contents

What is Discord Botly and Why Use It?

What is the Purpose of This Framework?

  1. Encourage separating logical parts of code into files.
  2. Encourage writing uniform, readable code.
  3. Abstract some of the bothersome parts of developing a Discord bot.

How Does it Achieve Those Things?

  1. Botly looks for files (aka. BotlyModules) in specified directories, imports the code and creates event listeners on the discord.js Client.

    For example, Botly would read all files from /commands, add a listener on interactionCreate event and when the event is emitted, Botly will find the matching slash command and call it's execute function (exported from file).

  2. All BotlyModules require similar code structure, so you can tell what's what at a glance.

  3. Botly provides multiple utilities, such as:
    • Methods for registering slash commands (so you don't have to mess around with the Discord API)
    • For ButtonInteraction and SelectMenuInteraction you can use Dynamic parameter ids
    • You can make filter modules, which are filters that automatically get applied to Botly Modules
    • You can make catch modules, which are functions that automatically catch all errors thrown from Botly Modules
    • Prefix command data is automatically gathered into one constant so you don't have to write help command manually, see prefixCommandData

The project code may look something like this:

.
|-commands
|   |-__catch.ts
|   |-ping.ts
|   |-balance.ts
|   └─admin
|       |-__filter.ts
|       └─ban.ts
|
|-events
|   |-reactions
|   |   |-messageReactionAdd.ts
|   |   └─messageReactionRemove.ts
|   └─ready.js
|
|-buttons
|   |-channel-clear-confirm.ts
|   └─reminder-[id]-subscribe.ts
|
|-selectMenus
|   └─give-[userId]-role.ts
|
|-prefixCommands
|   |-__catch.ts
|   |-__filter.ts
|   |-ping.ts
|   └─help.ts
|
|-index.ts
|-.env
└─package.json

Here Botly will automatically gather the modules from the events, commands, prefixCommands, buttons, and selectMenu directories and assign them to event listeners.

For better organization, modules can be infinitely nested into sub-directories.

Usage

In the examples below we will be using TypeScript. JavaScript can also be used, however TypeScript is recommended.

If you wish to see how Discord Botly is used in a real project, you can view Dual Bot.

Initialization

import { Client, Intents } from 'discord.js'
import botly from 'discord-botly'
import path from 'path'
import 'dotenv/config' // Load .env

// Initialize the Discord client
const client = new Client({
    intents: [
        Intents.FLAGS.GUILDS,
        Intents.FLAGS.GUILD_MESSAGES,
        Intents.FLAGS.GUILD_MEMBERS,
    ],
});

// Initialize discord-botly
botly.init({
    client,
    prefix: '!', // Optional
    eventsDir: path.join(__dirname, './events'), // Optional
    commandsDir: path.join(__dirname, './commands'), // Optional
    buttonsDir: path.join(__dirname, './buttons'), // Optional
    selectMenuDir: path.join(__dirname, './selectMenus'), // Optional
    prefixCommandDir: path.join(__dirname, './prefixCommands') // Optional
});

client.login(process.env.TOKEN);

The init method provides Botly the client and points to where the modules are located.

fieldvaluedescription
clientDiscord.Clientthe used discord.js client
prefix (optional)string | functionthe prefix to use for prefix commandsoptionally, this can be a function that returns a string to allow for per-guild prefixes
eventsDir (optional)stringthe absolute path to the event module directory
commandsDir (optional)stringthe absolute path to the slash command module directory
buttonsDir (optional)stringthe absolute path to the button interaction module directory
selectMenuDir (optional)stringthe absolute path to the select menu interaction module directory
prefixCommandDir (optional)stringthe absolute path to the prefix command module directory

If you want your bot to have different prefixes (for prefix commands) for different guilds, then you can give a function to the prefix field in init function.

The function can be either synchronous or asynchronous, and must return a string.

botly.init({
    client,
    prefix: (message) => database.settings.getPrefix(message.guild.id),
    prefixCommandDir: path.join(__dirname, './prefixCommands')
});

BotlyModules

A BotlyModule describes what code to run on the specified action.

There are 5 types of modules:

  • event - BotlyModule<'ready' | 'messageReactionAdd' | ...>
  • slash command - BotlyModule<CommandInteraction>
  • prefix command - BotlyModule<Message>
  • button - BotlyModule<ButtonInteraction>
  • select menu - BotlyModule<SelectMenuInteraction>

Code Example

The code in a BotlyModule follows the same pattern:

import type { BotlyModule } from 'discord-botly'

export const {
    // Must be exported by all modules
    execute,
    // (Required only for slash commands) Used only for slash commands
    commandData,
}: BotlyModule<moduleType> = {
    // actual implementation here ...
}

For more detailed samples see code-samples.md

Module Exports

Discord Botly searches for specific exports in the BotlyModule files:

export nametypemodule typerequireddescription
executefunction*truecan run any code, for example, replies 'pong' for '/ping' command
commandDatainstanceof SlashCommandBuilderslash commandtruethe slash command data, use @discordjs/builders
descriptionstringprefix commandfalsea description of the prefix command, used for prefixCommandData
syntaxstringprefix commandfalsethe syntax for the prefix command, used for prefixCommandData
categorystringprefix commandfalsewhat category the prefix command is in, used for prefixCommandData
aliasesstring[]prefix commandfalsealiases for the command (eg. !balance command may have an alias of !bal) prefixCommandData

Callback Parameters

The parameters given to the execute function as well as filter modules depend on the type of module.

module typeparameters
eventevent params (eg. ready.ts: (Client<true>), interactionCreate.ts: (Interaction))
slash command[interaction: CommandInteraction]
prefix command[message: Message, args: string[]]
button[interaction: ButtonInteraction, params: {key: string: string}]
select menu[interaction: SelectMenuInteraction, params: {key: string: string}]

The Filename is Important

Botly looks at the filename for information on what to do with the module.

module typepurposeexamplewill match
eventsinterpreted as the event nameready.ts, messageReactionAdd.ts
slash commandsinterpreted as the command nameping.ts, ban.ts/ping, /ban
prefix commandsinterpreted as the commandping.ts, ban.ts!ping, !ban (if prefix is set to !)
buttons and select menusinterpreted as the button's or select menu's customId, can include Dynamic parameter idsgive-member-guest-role.ts, reminder-[id]-subscribe.tsinteraction with customId of give-member-guest-role, reminder-21425252661616-subscribe

Dynamic Parameter Ids

In many cases you may want to include dynamic values in the ButtonInteraction.customId or SelectMenuInteraction.customId.

For this case Botly allows you to use dynamic parameters in the filename. Botly then automatically creates a RegExp that matches any customId based on the filename.

You can add a dynamic parameter to a filename by wrapping the parameter name in [] brackets.

For example, the file reminder-[id]-subscribe.ts will create the RegExp /^reminder-(.+)-subscribe$/ that would match reminder-294716294572593701-subscribe.

The parameters are passed to all callbacks as the second param inside an object. So of the filename is reminder-[id]-subscribe.ts, then the callback will receive ButtonInteraction, { id: string }.

filenameRegExpexample customIdcallback parameters
reminder-id-unsubscribe.ts/^reminder-(.+)-unsubscribe/reminder-294716294572593701-subscribe{ id: string }
get-role-id.ts/^get-role-(.+).ts/get-role-294716294572593701{ id: string }
give-user-userId-role-roleId-confirm.ts/^give-user-(.+)-role-(.+)-confirm$/give-user-294716294572593701-role-294716294572593701-confirm{ userId: string, roleId: string }

If a dynamic id has multiple parameters, eg. give-user-[userId]-role-[roleId]-confirm.ts then the parameters must have different names. So give-user-[id]-role-[id]-confirm.ts is not valid and will throw an error.

The parameters will be provided in the execute function:

// buttons/give-user-[userId]-role-[roleId]-confirm.ts
import type { BotlyModule } from 'discord-botly'
import type { ButtonInteraction } from 'discord.js'

export const { execute }: BotlyModule<ButtonInteraction> = {
    execute: (interaction, params) => {
        console.log(params.userId)
        console.log(params.roleId)
    }
}

Filter Modules

If you need create filters for multiple Botly Modules, such as checking that the user can use admin commands, you can use filter modules.

Filter modules are files with the name __filter.(js/ts) that export a filter function. The filter gets applied to all Botly Modules in the directory and its sub-directories.

prefixCommands
    |-admin
    |   | __filter.ts
    |   |-ban.ts
    |   └─kick.ts
    |
    |-__filter.ts
    |-ping.ts
    └─help.ts

In this example we have 2 filter modules:

  • prefixCommands/__filter.ts
  • prefixCommands/admin/__filter.ts

Since prefixCommands/__filter.ts is in the prefixCommands directory, it gets applied to all prefix commands.

prefixCommands/admin/__filter.ts however, gets applied ony to commands in the admin directory.

The actual code is super simple: it's just a function that returns a boolean or Promise<boolean>. The function must be a default export. The parameters passed to the filter function depend on the directory (see callback parameters) ie. filter in prefixCommands dir will recieve message: Message, args: string[], slashCommand: interaction: CommandInteraction etc. You can use FilterFunction type exported from discord-botly (code sample).

// Example of `prefixCommands/admin/__filter.ts`
export default async function(message: Message): Promise<boolean> {
    const { adminUsers } = await database.settings.getAdminUsers(message.guildId!);
    if (!adminUsers.includes(message.author.id)) {
        await message.reply('You do not have permission to use this command');
        return false;
    }
    else return true;
}

The above example for prefixCommands/admin/__filter.ts would check whether the message author is an admin the guild and return true or false appropriately. This filter would be applied to all commands in the admin directory and any sub-directories.

Catch modules

Catch modules allow you to easily handle errors for multiple botly modules as well as filter modules.

Just like with filter modules catch modules are files with the name __catch.(js/ts) that export a default function.

Whenever an error is thrown in any relevant botly module or filter module, it gets sent to the catch module.

import UserInputError from '../errors/UserInputError'
import type { CommandInteraction } from 'discord.js'
import type { CatchFunction } from 'discord-botly'

const catcher: CatchFunction<CommandInteraction> = async (error, interaction) => {
    if (error instanceof UserInputError)
        await interaction.reply({ ephemeral: true, content: error.message })
    else {
        await interaction.reply({ ephemeral: true, content: 'Oops, something has gone wrong...' })
        console.error(error.message)
    }
}

export default catcher

Catch modules get applied to the directory they are in and it's sub-directories (same as filter modules).

commands
    |-admin
    |   | __catch.ts
    |   |-ban.ts
    |   └─kick.ts
    |
    |-__catch.ts
    └─ping.ts

In the above example we have 2 catch modules:

  • commands/__catch.ts
  • commands/admin/__catch.ts

commands/__catch.ts will catch all errors thrown in the commands directory and it's sub-directories. While commands/admin/__catch.ts will only catch errors in the admin directory.

IMPORTANT: You may encounter that sometimes an error is not caught. This in not an issue with discord-botly, but rather a problem with Node.js.

See how to avoid it in code-samples

For more details, check out these articles:

Utils

Registering Slash Commands

For registering slash commands, you can use registerGlobalSlashCommands function that will register the commands for all guilds that the bot is in.

It required the client to be logged in, so call it on the ready event

// events/ready.ts
import { registerGlobalSlashCommands } from 'discord-botly' 
import type { BotlyModule } from 'discord-botly'

export const { execute }: BotlyModule<'ready'> = {
    execute: client => {
        registerGlobalSlashCommands(client)
    }
}

Prefix Command Data

This is a utility that automatically gathers the name, description, syntax and category for all of your prefix commands in one easy-to-access place.

For an example on how prefixCommandData can be used, see code-samples

import { prefixCommandData } from 'discord-botly'

console.log(prefixCommandData())
/*
    Logs: [
        { name: 'help', description: 'See available commands', syntax: 'help', category: undefined, aliases: ['h'] },
        { name: 'ban', description: 'Ban a member', syntax: 'ban @member', category: 'admin', aliases: [] },
        { name: 'coin', description: 'Toss a coin', syntax: 'coin', category: 'games', aliases: ['c'] },
    ]
*/

The name, description, syntax, category and aliases are automatically gathered from the prefix command modules.

The name comes from the filename, and the rest comes from exports.

// prefixCommands/games/coin.ts

import type { Message } from 'discord.js'
import type { BotlyModule } from 'discord-botly'

export const {
    execute,
    category,
    description,
    syntax,
    aliases,
}: BotlyModule<Message> = {
    description: 'Replies with a greeting',
    syntax: '!hello <name>',
    category: 'games',
    aliases: ['c'],
    execute: message => message.reply(Math.random() > 0.5 ? 'You got tails!' : 'You got heads!'),
}
1.9.1

2 years ago

1.8.2

2 years ago

1.9.0

2 years ago

1.5.3

2 years ago

1.7.0

2 years ago

1.6.0

2 years ago

1.10.0

2 years ago

1.4.4

2 years ago

1.4.3

2 years ago

1.4.2

2 years ago

1.4.0

2 years ago

1.3.1

2 years ago

1.2.8

2 years ago

1.2.5

2 years ago

1.2.4

2 years ago

1.2.3

2 years ago

1.2.2

2 years ago

1.2.1

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.2.0-alpha.1.0

2 years ago

0.1.1-alpha.1.0

2 years ago