UNPKG

djs-easy

Version:

djs-easy is a javascript package for discord with the a lot stuff you need to make your bot programming easy

245 lines (195 loc) 8.68 kB
const fs = require('fs'); const Discord = require('discord.js'); const { Client, Collection } = Discord; const { defaults, defObjVal, getObjVal, escapeRegExpChars } = require('./utils.js'); const Arguments = require('./structures/Arguments.js'); const Call = require('./structures/Call.js'); const Cooldown = require('./structures/Cooldown.js'); const Prompt = require('./structures/Prompt.js'); /** * @typedef {object} Command * @property {string} id * @property {function(call: Call): void} exec * @property {string[]} aliases * @property {'DM'|'GUILD_TEXT'|'ANY'} channels * @property {?string} category */ function load(commands, path, customProps, category, editCategory) { for (let file of fs.readdirSync(path)) { try { if (!file.endsWith('.js')) continue; let command = require(path + '/' + file); let id = defObjVal(command, customProps.id, (id) => id || undefined); let exec = defObjVal(command, customProps.exec, (exec) => exec || undefined); if (typeof id !== 'string' || typeof exec !== 'function') throw new TypeError('Either command.' + customProps.id + ' or command.' + customProps.exec + ' are not their proper values.'); if (category && !command.category) command.category = editCategory(category); defObjVal(command, customProps.aliases, (aliases) => Array.isArray(aliases) ? aliases.filter((alias) => typeof alias === 'string').map((alias) => alias.toLowerCase()) : []); defObjVal(command, customProps.channels, (channels) => channels || 'ANY'); defObjVal(command, customProps.canUse, (obj) => typeof obj === 'object' && obj !== null ? obj : {}); defObjVal(command, customProps.cooldown, (cooldown) => cooldown instanceof Cooldown); commands.set(id.toLowerCase(), command); } catch (err) { console.warn(file + ' command failed to load.\n', err.stack); } } } function canUse(rules, message) { return (!rules.users && !rules.roles) || (!!rules.users && rules.users.includes(message.author.id)) || (!!rules.roles && !!message.member && message.member.roles.cache.some((r) => rules.roles.includes(r.id))); } /** * @typedef {object} HandleOptions * @property {?string|string[]|function(message: Discord.Message): Promise<string|string[]>} customPrefix The prefix(es) of the bot. A function should * return a string or array of strings. If a database call or some other asynchronous action is required, the function should return a Promise. * @property {?function(message: Discord.Message, cmd: Command, err: any): void} onError A function called with the message, the command and the error * when a command encounters an error upon being run. * @property {?function(category: string|boolean): string} editCategory Used to edit the string passed into the category property of the command. * Requires `setCategoryProperty` to be true. * @property {?string} defaultCategory The default category that is set on a command if it has no category folder. * @property {?boolean} loadCategories A boolean option to load the folders inside the commands folder as well. * @property {?boolean} setCategoryProperty A boolean option representing whether or not to set the category property of a command based off of it's * parent folder. * @property {?boolean} defaultPrefix A boolean option determining if the default mention prefix is used, e.g `@bot ping`. * @property {?boolean} allowBots A boolean option on whether or not to allow commands to be triggered by bots. * @property {?Discord.Snowflake[]} restrictedGuilds Restricts commands to certain guilds. * @property {?object} customProps Redefines the property locations of a command, e.g. `{ id: 'name', exec: 'run' }` changes the location of the * command id to `command.name` and the command execution to `command.run`. You can also use deep properties such as `{ id: 'info.name' }`. * @property {?Discord.ClientOptions} clientOptions Options to supply directly to the `Client` instance being created. Is not used if the `token` * parameter is supplied. */ /** * @param {string} location The path to the commands folder. * @param {Discord.Client|string} token A token to create a `Client` instance and login with, or a pre-existing `Client` instance to use. * @param {HandleOptions} options Options to use with the handle function. * @returns {Discord.Client} */ function handler(location, token, { customPrefix = '!', onError = (_, command, exc) => console.warn('The ' + command.id + ' command encountered an error:\n' + exc.stack), editCategory = (category) => category.replace(/^./, (m) => m.toUpperCase()), defaultCategory = 'Other', loadCategories = true, setCategoryProperty = true, defaultPrefix = true, allowBots = false, restrictedGuilds = [], customProps = {}, clientOptions, commandEmitter } = {}) { defaults(customProps, { id: 'id', exec: 'exec', aliases: 'aliases', channels: 'channels', canUse: 'canUse', cooldown: 'cooldown' }); let determinePrefix = typeof customPrefix === 'function' ? customPrefix : () => customPrefix; let client = typeof token === 'string' ? new Client(clientOptions) : token; let commands = new Collection(); commandEmitter = commandEmitter || client; handler.commands = commands; load(commands, location, customProps, setCategoryProperty ? defaultCategory : false, editCategory); if (loadCategories === true) for (let folder of fs.readdirSync(location)) if (fs.statSync(location + '/' + folder).isDirectory()) load(commands, location + '/' + folder, customProps, setCategoryProperty ? folder : false, editCategory); client.on('messageCreate', async (message) => { if (message.channel.partial) await message.channel.fetch(); if (message.author.bot && !allowBots) return; let prompt = handler.prompts.find((p) => p.user.id === message.author.id && p.channel.id === message.channel.id); if (prompt) return prompt.addInput(message); let prefixes = await determinePrefix(message); prefixes = (Array.isArray(prefixes) ? prefixes : [prefixes]).filter((p) => typeof p === 'string').map(escapeRegExpChars); if (prefixes.length === 0) return; if (defaultPrefix) prefixes.push('<@' + client.user.id + '>', '<@!' + client.user.id + '>'); let prefixUsed = message.content.match(new RegExp('^' + prefixes.join('|'))); if (prefixUsed == null) return; prefixUsed = prefixUsed[0]; let cut = message.content.substring(prefixUsed.length).trim(); let args = cut.split(/\s+/g); if (!args[0]) return; let aliasUsed = args[0].toLowerCase(); let command = commands.find((cmd) => aliasUsed.toLowerCase() === getObjVal(cmd, customProps.id) || getObjVal(cmd, customProps.aliases).includes(aliasUsed)); if (command == null) return; let rules = getObjVal(command, customProps.canUse); if (!canUse(rules, message)) return typeof rules.cant === 'function' ? rules.cant(message) : message.channel.send(rules.cant); let channels = getObjVal(command, customProps.channels); if ( (message.guild && restrictedGuilds.length > 0 && !restrictedGuilds.includes(message.guild.id)) || ( channels === 'ANY' ? false : channels === 'TEXT' ? !message.channel.isText() : message.channel.type !== channels ) ) return; if (command.cooldown && command.cooldown.onCooldown(message.author.id)) return command.cooldown.handle(message); cut = cut.substring(aliasUsed.length).trim(); args.shift(); try { let call = new handler.Call(message, command, commands, cut, null, prefixUsed, aliasUsed, handler); call.args = command.arguments ? await command.arguments.getAll(call) : args; let result = await getObjVal(command, customProps.exec)(call); commandEmitter.emit('commandUsed', call, result); } catch (exc) { onError(message, command, exc); } }); if (typeof token === 'string') client.login(token); return client; } /** The Promise class (purely for redefining and using a promise library different than the native js one, such as bluebird). */ handler.Promise = Promise; handler.Arguments = Arguments; handler.Call = Call; handler.Cooldown = Cooldown; handler.Prompt = Prompt; Call.handler = handler; /** * All current `Prompt` instances. * @type {Array<Prompt>} */ handler.prompts = []; /** * The default prompt options. Adjusted purely for code convenience * @type {PromptOptions} */ handler.promptOptionsDefaults = { filter: () => true, correct: () => {}, formatCorrect: (_, arg) => arg, formatTrigger: (_, arg) => arg, cancellable: true, autoRespond: true, addLastMatch: false, time: 180000, messages: 1, attempts: 10 }; module.exports = handler;