const { info } = require("./Logger");
const Command = require("./Command");
const { Collection } = require("discord.js");
const path = require("path");
/** Handles the loading of commands */
class CommandLoader {
/** @param {ModzClient} [client] - Client to use */
constructor(client) {
/**
* The client the loader is serving
* @name CommandLoader#Client
* @type {ModzClient}
* @readonly
*/
Object.defineProperty(this, "client", { value: client });
/**
* The loaded commands
* @type {Collection<string, Command>}
*/
this.commands = new Collection();
/**
* The loaded aliases
* @type {Collection<string, Command>}
*/
this.aliases = new Collection();
/**
* The path to the commands
* @type {?string}
*/
this.commandsPath = null;
/**
* The path to the commands
* @type {string[]}
*/
this.defaultCommands = [];
}
/**
* Loads a single command
* @param {Command|Function} command - Either a Command instance, or a constructor for one
* @return {CommandLoader}
* @see {@link CommandLoader#loadCommands}
*/
loadCommand(command) {
return this.loadCommands([command]);
}
/**
* Loads multiple commands
* @param {Command[]|Function[]} commands - An array of Command instances, or constructors
* @return {CommandLoader}
*/
loadCommands(commands) {
if (!Array.isArray(commands)) throw new TypeError("Commands must be an Array.");
for (let command of commands) {
if (typeof command === "function") command = new command(this.client); // eslint-disable-line new-cap
// Verify that it's an actual command
if (!(command instanceof Command)) {
this.client.emit("warn", `Attempting to register an invalid command object: ${command}; skipping.`);
continue;
}
// Make sure there aren't any conflicts
if (this.commands.some(cmd => cmd.name === command.name || cmd.aliases.includes(command.name))) {
throw new Error(`A command with the name/alias "${command.name}" is already registered.`);
}
for (const alias of command.aliases) {
if (this.commands.some(cmd => cmd.name === alias || cmd.aliases.includes(alias))) {
throw new Error(`A command with the name/alias "${alias}" is already registered.`);
}
}
// Add the command
this.commands.set(command.name, command);
info(`[DISCORD] Loading command ${command.name}...`);
command.aliases.forEach(alias => { // eslint-disable-line max-nested-callbacks
this.aliases.set(alias, command.name);
});
/**
* Emitted when a command is loaded
* @event ModzClient#commandLoad
* @param {Command} command - Command that was loaded
*/
this.client.emit("commandLoad", command);
}
return this;
}
/**
* Loads all commands in a directory. The files must export a Command class constructor or instance.
* @param {string|RequireAllOptions} options - The path to the directory, or a require-all options object
* @return {CommandLoader}
*/
loadCommandsIn(options) {
const obj = require("require-all")(options);
const commands = [];
for (const group of Object.values(obj)) {
for (let command of Object.values(group)) {
if (typeof command.default === "function") command = command.default;
commands.push(command);
}
}
if (typeof options === "string" && !this.commandsPath) this.commandsPath = options;
return this.loadCommands(commands);
}
/**
* Loads the default commands
* @param {Object} [options] - The object to specify which commands not to register
* @param {boolean} [options.eval_=true] - Whether or not to load the built-in `eval` command
* @param {boolean} [options.exec=true] - Whether or not to load the built-in `exec` command
* @param {boolean} [options.help=true] - Whether or not to load the built-in `help` command
* @param {boolean} [options.ping=true] - Whether or not to load the built-in `ping` command
* @param {boolean} [options.stats=true] - Whether or not to load the built-in `stats` command
* @return {CommandLoader}
*/
loadDefaultCommands({ eval_ = true, exec = true, help = true, ping = true, stats = true, reload = true } = {}) {
if (eval_) this.loadCommand(require("../commands/util/eval")); this.defaultCommands.push("eval");
if (exec) this.loadCommand(require("../commands/util/exec")); this.defaultCommands.push("exec");
if (help) this.loadCommand(require("../commands/util/help")); this.defaultCommands.push("help");
if (ping) this.loadCommand(require("../commands/util/ping")); this.defaultCommands.push("ping");
if (stats) this.loadCommand(require("../commands/util/stats")); this.defaultCommands.push("stats");
if (reload) this.loadCommand(require("../commands/util/reload")); this.defaultCommands.push("reload");
return this;
}
/**
* Resolves a command file path from a command's group ID and memberName
* @param {string} group - The command's group
* @param {string} name - Name of the command
* @return {string} Fully-resolved path to the corresponding command file
*/
resolveCommandPath(group, name) {
return path.join(this.commandsPath, group, `${name}.js`);
}
reloadCommand(command, oldCommand) {
if (typeof command === "function") command = new command(this.client); // eslint-disable-line new-cap
if (command.name !== oldCommand.name) throw new Error("Command name cannot change.");
if (command.group !== oldCommand.group) throw new Error("Command group cannot change.");
this.commands.set(command.name, command);
/**
* Emitted when a command is reregistered
* @event ModzClient#commandReload
* @param {Command} newCommand - New command
* @param {Command} oldCommand - Old command
*/
this.client.emit("commandReload", command, oldCommand);
this.client.emit("debug", `Reloaded command ${command.name}.`);
}
/**
* Finds all commands that match the search string
* @param {string} [searchString] - The string to search for
* @param {boolean} [exact=false] - Whether the search should be exact
* @return {Command[]} All commands that are found
*/
findCommands(searchString = null, exact = false) {
if (!searchString) return this.commands;
// Find all matches
const lcSearch = searchString.toLowerCase();
const matchedCommands = this.commands.filterArray(
exact ? commandFilterExact(lcSearch) : commandFilterInexact(lcSearch)
);
if (exact) return matchedCommands;
// See if there's an exact match
for (const command of matchedCommands) {
if (command.name === lcSearch || (command.aliases && command.aliases.some(ali => ali === lcSearch))) {
return [command];
}
}
return matchedCommands;
}
}
function commandFilterExact(search) {
return cmd => cmd.name === search
|| (cmd.aliases && cmd.aliases.some(ali => ali === search))
|| `${cmd.name}` === search;
}
function commandFilterInexact(search) {
return cmd => cmd.name.includes(search)
|| `${cmd.name}` === search
|| (cmd.aliases && cmd.aliases.some(ali => ali.includes(search)));
}
module.exports = CommandLoader;