const Eris = require('eris');
const glob = require('glob');
const path = require('path');
const Logger = require('another-logger');
const reload = require('require-reload')(require);
const Command = require('./Command');
const Category = require('./Category');
const Component = require('./Component');
const constants = require('./constants');
const builtinCommandRequirements = require('./requirements');
const DEFAULT_CATEGORY = 'Default';
/**
* Aghanim Client extends from Eris.Client
* @extends Eris.Client
*/
class Client extends Eris.Client {
/**
* Create a client instance.
* @class
* @param {string} token - The token used to log into the bot.
* @param {Object} options - Options to start the client with. This object is also passed to Eris.
* If there are a aghanim.config.js/json in project root, that will be loaded instead object passed to constructor.
* See Eris Client constructor options https://abal.moe/Eris/docs/Client
* @param {string} [options.prefix = ''] - The prefix the bot will respond to in
* guilds for which there is no other confguration. (Currently everywhere)
* @param {boolean} [options.allowMention = false] - Whether or not the bot can respond
* to messages starting with a mention of the bot.
* @param {boolean} [options.ignoreBots = true] - Whether or not the bot ignoresBots. Default: true
* @param {boolean} [options.ignoreSelf = true] - Whether or not the bot ignores self. Default: true
* @param {string} [options.helpMessage = '**Help**'] - Title for default command help Message
* @param {string} [options.helpMessageAfterCategories = '**Note**: Use \`${options.prefix}help <category>\` to see the commands']
* Message after categories in default command help message are shown
* @param {boolean} [options.helpDM = true] - Active direct message to default command help
* @param {boolean} [options.helpEnable = true] - Enable/disable default command help
*/
constructor(token, options = {}) {
// Attempt to load options from aghanim.config.js(on) file
let configurationFileFound = false;
if (process.env.AGHANIM_CONFIG_FILE) {
try {
const configFile = process.env.AGHANIM_CONFIG_FILE;
options = require(configFile); /* eslint import/no-dynamic-require: "off", global-require : "off", no-param-reassign : "off" */
configurationFileFound = configFile;
} catch (err) {
console.warn({ err });
}
} else {
try {
const configFile = `${process.cwd()}/aghanim.config`;
options = require(configFile); /* eslint import/no-dynamic-require: "off", global-require : "off", no-param-reassign : "off" */
configurationFileFound = configFile;
} catch (err) {
console.warn({ err });
} /* eslint no-empty: "off" */
}
super(token, options);
// Logger
this._logger = new Logger({
label: 'Aghanim',
timestamps: true,
...(options.logger || {})
});
if (configurationFileFound) {
this._logger.info(
`Loaded: aghanim.config.js(on) from ${configurationFileFound}`
);
}
/** @prop {string} - The prefix the bot will respond to in guilds
* for which there is no other confguration. */
this.prefix = options.prefix || '';
/** @prop {Command[]} - An array of commands the bot will respond to. */
this.commands = [];
/** @prop {Category[]} - Categories for commands. */
this.categories = [];
/** @prop {Command[]} - An array of commands the bot will respond to. */
this.interactionCommands = [];
/** @prop {Category[]} - Categories for commands. */
this.interactionCommandCategories = [];
/** @prop {Object<Component>} - Components. */
this.components = {};
/** @prop {Object} - Setup */
this.setup = {};
// /** @prop {Object} - Context Extension */
// this.contextExtension = {}
this._ready = false;
this._commandsRequirements = {};
// /** @prop {boolean} - Whether or not the bot can respond to messages
// * starting with a mention of the bot. Defaults to true. */
// this.allowMention = options.allowMention === null ? false : options.allowMention
/** @prop {boolean} - Whether or not the bot ignores messages
sent from bot accounts. Defaults to true.
* @default true */
this.ignoreBots =
options.ignoreBots !== undefined ? options.ignoreBots : true;
/** @prop {boolean} - Ignore self
* @default true */
this.ignoreSelf =
options.ignoreSelf !== undefined ? options.ignoreSelf : true;
this.once('ready', () => {
if (this._ready) {
return;
}
this._ready = true;
/**
* @prop {RegExp} - The RegExp used to tell whether or not a message starts
* with a mention of the bot. Only present after the 'ready' event.
*/
this.mentionPrefixRegExp = new RegExp(`^<@!?${this.user.id}>\\s?`);
this.getOAuthApplication().then((app) => {
/**
* @prop {object} - The OAuth application information returned by
* Discord. Present some time after the ready event.
* @prop {string} description - Discord App description
* @prop {string} name - Discord App name
* @prop {string} owner - Discord App owner
* @prop {string} owner.id - Owner ID
* @prop {string} owner.username - Owner username
* @prop {string} owner.discriminator - Owner discriminator
* @prop {string} owner.avatar - Owner avatar
* @prop {boolean} bot_public - If app is public
* @prop {boolean} bot_require_code_grant -
* @prop {string} id - Discord App id
* @prop {string} icon - Discord App icon
*/
this.app = app;
/**
* Owner description
* @prop {string} id - Owner ID
* @prop {string} username - Owner username
* @prop {string} discriminator - Owner discriminator
* @prop {string} avatar - Owner avatar
* @prop {Ownersend} send - Send a message to Owner
*/
this.owner = Object.assign({}, this.app.owner);
this.getDMChannel(this.owner.id).then((channel) => {
/**
* Function to send messages to owner
* @callback Ownersend
* @param {string|EmbedMessageObject} content - Message content to send
* @param {object} file - File to send
*/
this.owner.send = function sendOwner(content, file) {
channel.createMessage(content, file);
};
});
this.handleEvent('ready')();
this.registerInteractionCommands();
});
})
.on('error', (err) => {
this._logger.error(err);
/**
* Fired when there are an error
* @event Client#aghanim:error
* @param {object} err - Error
* @param {Client} client - Client instance
*/
this.emit('aghanim:error', err, this);
})
.on('messageCreate', this.handleMessage)
.on('messageReactionAdd', this.handleEvent('messageReactionAdd'))
.on('messageReactionRemove', this.handleEvent('messageReactionRemove'))
.on('guildCreate', this.handleEvent('guildCreate'))
.on('guildDelete', this.handleEvent('guildDelete'))
.on('guildMemberAdd', this.handleEvent('guildMemberAdd'))
.on('guildMemberRemove', this.handleEvent('guildMemberRemove'))
.on('interactionCreate', this.handleInteractionCreate);
this.setup.helpMessage = `${options.helpMessage || '**Help**'}\n\n`;
this.setup.helpMessageAfterCategories = `${
options.helpMessageAfterCategories ||
`**Note**: Use \`${this.prefix}help <category>\` to see those commands`
}\n\n`;
this.setup.helpDM = options.helpDM || false;
if (!options.disableHelp) {
// Add default help command to bot
this.addCommand(
new Command('help', {}, async (msg, args, client, command) => {
/* eslint no-unused-vars: "off" */
const categories = client.categories.map((c) => c.name.toLowerCase());
const query = args.from(1).toLowerCase();
let { helpMessage } = client.setup;
if (categories.includes(query)) {
const cmds = client.getCommandsOfCategories(query);
if (!cmds) {
helpMessage +=
client.categories
.filter(
(c) => !c.hide
) /* eslint prefer-template: "off", max-len: "off" */
.map(
(c) =>
`**${c.name}** \`${
client.prefix
}help ${c.name.toLowerCase()}\` - ${c.help}`
)
.join('\n') +
'\n\n' +
client.setup.helpMessageAfterCategories;
} else {
helpMessage += cmds
.filter((c) => !c.hide)
.map(
(c) =>
`\`${client.prefix}${c.name}${
c.args ? ' ' + c.args : ''
}\` - ${c.help}${
c.childs.length
? '\n' +
c.childs
.filter((s) => !s.hide)
.map(
(s) =>
` · \`${s.name}${
s.args ? ' ' + s.args : ''
}\` - ${s.help}`
)
.join('\n')
: ''
}`
)
.join('\n');
}
} else if (categories.length) {
helpMessage +=
client.categories
.filter((c) => !c.hide)
.map(
(c) =>
`**${c.name}** \`${
client.prefix
}help ${c.name.toLowerCase()}\` - ${c.help}`
)
.join('\n') +
'\n\n' +
client.setup.helpMessageAfterCategories;
} else {
const cmds = client.getCommandsOfCategories('Default');
if (!cmds) {
helpMessage += 'No commands';
} else {
helpMessage += cmds
.filter((c) => !c.hide)
.map(
(c) =>
`\`${client.prefix}${c.name}${
c.args ? ' ' + c.args : ''
}\` - ${c.help}${
c.childs.length
? '\n' +
c.childs
.filter((s) => !s.hide)
.map(
(s) =>
` · \`${s.name}${
s.args ? ' ' + s.args : ''
}\` - ${s.help}`
)
.join('\n')
: ''
}`
)
.join('\n');
}
}
if (!client.setup.helpDM) {
return msg.channel.createMessage(helpMessage);
}
return msg.author
.getDMChannel()
.then((channel) => channel.createMessage(helpMessage));
})
);
}
}
async registerInteractionCommands() {
const interactionCommandScopeGlobal = this.interactionCommands.filter(
(interactionCommand) =>
!interactionCommand.scope ||
(interactionCommand.scope &&
interactionCommand.scope.type ===
constants.interactionCommandScope.global)
);
const interactionCommandScopeGuilds = this.interactionCommands.filter(
(interactionCommand) =>
interactionCommand.scope &&
interactionCommand.scope.type ===
constants.interactionCommandScope.guild
);
if (interactionCommandScopeGlobal.length) {
try {
// TODO: register, edit and delete global commands
this._logger.debug(
`Command interaction scope global: ${interactionCommandScopeGlobal
.map(({ name }) => name)
.join(', ')}`
);
const globalCommands = await this.getCommands();
for (const interactionCommand of interactionCommandScopeGlobal) {
const { name, description, type, options, customOptions } =
interactionCommand;
/* Global command to create */
if (
globalCommands.every((command) => command.name !== interaction.name)
) {
this.createCommand({ name, description, type, options }).then(
() => {
this._logger.info(
`Command interaction scope global created: ${name}`
);
}
);
/* Global command to edit */
} else if (customOptions && customOptions['dev.forceUpdate']) {
const command = globalCommands.find(
(command) => command.name === interaction.name
);
this.editCommand(command.id, {
name,
description,
type,
options
}).then(() => {
this._logger.info(
`Command interaction scope global edited: ${name}`
);
});
/* Global command to ignore the editing */
} else {
this._logger.debug(
`Command interaction scope global skipped to updating: ${name}. Use customOptions['dev.forceUpdate'] = true`
);
}
}
/* Global commands to remove */
const globalCommandsToRemove = globalCommands
// TODO: remove the commands that are disabled
.filter(
(globalCommand) =>
!interactionCommandScopeGlobal.some(
(interactionCommand) =>
interactionCommand.name === globalCommand.name
)
);
if (globalCommandsToRemove.length) {
this._logger.warn(
`Command interaction scope global commands to remove: ${globalCommandsToRemove
.map(({ name }) => name)
.join(', ')}`
);
this._logger.debug(
`Command interaction scope global registered commands: ${globalCommands
.map(({ name }) => name)
.join(', ')}`
);
globalCommandsToRemove.forEach((globalCommand) => {
this._logger.debug(
`Command interaction scope global to remove ${guildCommand.name}`
);
this.deleteCommand(guildID, globalCommand.id)
.then(() => {
this._logger.info(
`Command interaction scope global removed: ${guildCommand.name}`
);
})
.catch((error) => {
this._logger.error(
`Command interaction scope global error removing ${guildCommand.name}: ${error}`
);
});
});
}
} catch (error) {}
}
if (interactionCommandScopeGuilds.length) {
const guildIDs = new Set();
interactionCommandScopeGuilds.forEach((interactionCommandScopeGuild) =>
guildIDs.add(...interactionCommandScopeGuild.scope.guildIDs)
);
[...guildIDs].forEach(async (guildID) => {
try {
const interactionCommandsGuild = interactionCommandScopeGuilds.filter(
(interactionCommandScopeGuild) =>
interactionCommandScopeGuild.scope.guildIDs.includes(guildID)
);
this._logger.debug(
`Command interaction scope guild ID (${guildID}): ${interactionCommandsGuild
.map(({ name }) => name)
.join(', ')}`
);
const guildCommands = await this.getGuildCommands(guildID);
/* Guild command defined */
if (interactionCommandsGuild.length) {
for (const interaction of interactionCommandsGuild) {
const { name, description, type, options, customOptions } =
interaction;
this._logger.debug(
`Command interaction scope guild ID (${guildID}) check registration: ${name}`
);
try {
/* Guild command to create */
if (
guildCommands.every(
(command) => command.name !== interaction.name
)
) {
this._logger.debug(
`Command interaction scope guild ID (${guildID}) check registration: ${name} creating`
);
await this.createGuildCommand(guildID, {
name,
description,
type,
options
});
this._logger.info(
`Command interaction scope guild ID (${guildID}) created: ${name}`
);
/* Guild command to edit */
} else if (customOptions && customOptions['dev.forceUpdate']) {
const command = guildCommands.find(
(command) => command.name === interaction.name
);
this._logger.debug(
`Command interaction scope guild ID (${guildID}) check registration: ${name} editing`
);
await this.editGuildCommand(guildID, command.id, {
name,
description,
type,
options
});
this._logger.info(
`Command interaction scope guild ID (${guildID}) edited: ${name}`
);
/* Guild command to ignore the editing */
} else {
this._logger.debug(
`Command interaction scope guild ID (${guildID}) skipped to updating: ${name}. Use customOptions['dev.forceUpdate'] = true`
);
}
} catch (error) {
this._logger.debug(
`Command interaction scope guild ID (${guildID}) check registration: ${name} error: ${error.message}`
);
this.emit(
'aghanim:command-interaction:error:register',
interaction,
this,
{ guildID, error }
);
}
}
}
/* Guild commands to remove */
const guildCommandsToRemove = guildCommands
// TODO: remove the commands that are disabled
.filter(
(guildCommand) =>
!interactionCommandsGuild.some(
(interactionCommandGuild) =>
interactionCommandGuild.name === guildCommand.name
)
);
if (guildCommandsToRemove.length) {
this._logger.warn(
`Command interaction scope guild ID (${guildID}) guild commands to remove: ${guildCommandsToRemove
.map(({ name }) => name)
.join(', ')}`
);
this._logger.debug(
`Command interaction scope guild ID (${guildID}) registered commands: ${guildCommands
.map(({ name }) => name)
.join(', ')}`
);
for (const guildCommand of guildCommandsToRemove) {
try {
this._logger.debug(
`Command interaction scope guild ID (${guildID}) to remove ${guildCommand.name}`
);
await this.deleteGuildCommand(guildID, guildCommand.id);
this._logger.info(
`Command interaction scope guild ID (${guildID}) removed: ${guildCommand.name}`
);
} catch (error) {
this._logger.error(
`Command interaction scope guild ID (${guildID}) error removing ${guildCommand.name}: ${error}`
);
}
}
}
} catch (error) {
// TODO: handle the error
}
});
}
}
/**
* Given a message, see if there is a command and process it if so.
* @param {Object} msg - The message object recieved from Eris.
* @return {*} - Returns
*/
async handleMessage(msg) {
if (!this._ready) return;
if (!this.triggerMessageCreate(msg, this)) {
return;
}
this.handleEvent('messageCreate')(msg);
if (this.ignoreBots && msg.author.bot) return;
if (this.ignoreSelf && msg.author.id === this.user.id) return;
const args = this.createCommandArgs(msg);
if (!args) {
return;
}
const command = this.getCommandByName(args[0], args[1]);
if (!command) {
return;
}
// const context = Object.assign({
// command,
// client: this
// }, this.contextExtension)
try {
/**
* Fired before a command is executed. Can't stop command of running
* @event Client#aghanim:command:prereq
* @param {object} msg - Eris Message object
* @param {args} args - Args object
* @param {Client} client - Client instance
* @param {Command} command - Command
*/
this.emit('aghanim:command:prereq', msg, args, this, command);
await command.runHook('prereq', msg, args, this, command);
const notpass = !(await this.checkRequirements(msg, args, this, command));
if (notpass) return;
/**
* Fired before a command is executed. Can't stop command of running
* @event Client#aghanim:command:prerun
* @param {object} msg - Eris Message object
* @param {args} args - Args object
* @param {Client} client - Client instance
* @param {Command} command - Command
*/
this.emit('aghanim:command:prerun', msg, args, this, command);
await command.runHook('prerun', msg, args, this, command);
if (command.response) {
switch (typeof command.response) {
case 'string': {
await msg.channel.createMessage(command.response);
break;
}
case 'function': {
const response = command.response(msg, args, this, command);
await msg.channel.createMessage(response);
break;
}
case 'object': {
await msg.channel.createMessage(command.response);
break;
}
default: {
}
}
} else if (command.responseDM) {
switch (typeof command.responseDM) {
case 'string': {
await msg.channel.createMessage(command.responseDM);
break;
}
case 'function': {
const responseDM = command.responseDM(msg, args, this, command);
await msg.channel.createMessage(responseDM);
break;
}
case 'object': {
await msg.channel.createMessage(command.responseDM);
break;
}
default: {
}
}
} else {
await command.run(msg, args, this, command);
}
/**
* Fired after a command is executed. Don't cant stop command of running
* @event Client#aghanim:command:executed
* @param {object} msg - Eris Message object
* @param {args} args - Args object
* @param {Client} client - Client instance
* @param {Command} command - Command
*/
this.emit('aghanim:command:executed', msg, args, this, command);
await command.runHook('executed', msg, args, this, command);
} catch (err) {
/**
* Fired when a command got an error executing the run function
* @event Client#aghanim:command:error
* @param {object} err - Error
* @param {object} msg - Eris Message object
* @param {args} args - Args object
* @param {Client} client - Client instance
* @param {Command} command - Command
*/
this._logger.error(`${command.name} - ${err} - ${err.stack}`);
this.emit('aghanim:command:error', err, msg, args, this, command);
try {
await command.runHook('error', msg, args, this, command, err);
} catch (errhook) {
this._logger.error(`${command.name} - ${errhook} - ${errhook.stack}`);
this.emit('aghanim:command:error', errhook, msg, args, this, command);
}
}
}
/**
* Given a message, see if there is a command and process it if so.
* @param {Object} interaction - The interaction object recieved from Eris.
* @return {*} - Returns
*/
async handleInteractionCreate(interaction) {
if (interaction instanceof Eris.CommandInteraction) {
const interactionCommand = this.interactionCommands.find(
(interactionCommand) =>
interactionCommand.name === interaction.data.name
);
if (!interactionCommand) {
this._logger.warn(
`Command interaction triggered but the runner was not found: ${interaction.data.name}`
);
/**
* Fired when an interaction command is triggered but the runner was not found
* @event Client#aghanim:command-interaction:triggered-not-found
* @param {object} interaction - Error
* @param {Client} client - Client instance
* @param {Command} interactionCommand - Interaction command
*/
this.emit(
'aghanim:command-interaction:triggered-not-found',
interaction,
this,
interactionCommand
);
return;
}
interaction.user = interaction.user || interaction.member.user;
this._logger.debug(
`Command interaction triggered: ${interactionCommand.name}`
);
/**
* Fired when an interaction command is triggered
* @event Client#aghanim:command-interaction:triggered
* @param {object} interaction - Error
* @param {Client} client - Client instance
* @param {Command} interactionCommand - Interaction command
*/
this.emit(
'aghanim:command-interaction:triggered',
interaction,
this,
interactionCommand
);
try {
await interactionCommand.runHook(
'trigger',
interaction,
this,
interactionCommand
);
interactionCommand.customOptions &&
interactionCommand.customOptions.defer &&
(await interaction.defer());
if (
interactionCommand.requirements &&
interactionCommand.requirements.length
) {
for (const requirement of interactionCommand.requirements) {
if (
!(await requirement.validate(
interaction,
this,
interactionCommand,
requirement
))
) {
if (requirement.response) {
switch (typeof requirement.response) {
case 'string': {
return await interaction.createMessage(
requirement.response
);
break;
}
case 'object': {
return await interaction.createMessage(
requirement.response
);
break;
}
case 'function': {
const interactionResponse = await requirement.response(
interaction,
this,
interactionCommand,
requirement
);
return (
interactionResponse &&
(await interaction.createMessage(interactionResponse))
);
break;
}
default:
break;
}
} /*else if(requirement.run){
switch (typeof requirement.response) {
case 'string':{
return interaction.response
break;
}
case 'function':{
return interaction.response(interaction, this, interactionCommand, requirement)
break;
}
default:
return interaction.run(interaction, this, interactionCommand, requirement)
break;
}
}*/
}
}
}
this._logger.debug(
`Command interaction executing: ${interactionCommand.name}`
);
/**
* Fired when an interaction command runner go to be executed
* @event Client#aghanim:command-interaction:executing
* @param {object} interaction - Error
* @param {Client} client - Client instance
* @param {Command} interactionCommand - Interaction command
*/
this.emit(
'aghanim:command-interaction:executing',
interaction,
this,
interactionCommand
);
await interactionCommand.run(interaction, this, interactionCommand);
this._logger.info(
`Command interaction executed: ${interactionCommand.name}`
);
/**
* Fired when an interaction command runner was executed
* @event Client#aghanim:command-interaction:executed
* @param {object} interaction - Error
* @param {Client} client - Client instance
* @param {Command} interactionCommand - Interaction command
*/
this.emit(
'aghanim:command-interaction:executed',
interaction,
this,
interactionCommand
);
this._logger.debug(
`Command interaction running hook: execute: ${interactionCommand.name}`
);
await interactionCommand.runHook(
'execute',
interaction,
this,
interactionCommand
);
this._logger.debug(
`Command interaction run hook: execute: ${interactionCommand.name}`
);
} catch (err) {
/**
* Fired when a command got an error executing the run function
* @event Client#aghanim:command:error
* @param {object} err - Error
* @param {object} msg - Eris Message object
* @param {args} args - Args object
* @param {Client} client - Client instance
* @param {Command} command - Command
*/
this._logger.error(
`Command interaction error: ${interactionCommand.name} - ${err.message} - ${err.stack}`
);
/**
* Fired when an interaction command had an error
* @event Client#aghanim:command-interaction:error
* @param {object} interaction - Error
* @param {Client} client - Client instance
* @param {Command} interactionCommand - Interaction command
*/
this.emit(
'aghanim:command-interaction:error',
err,
interaction,
this,
interactionCommand
);
try {
await interactionCommand.runHook(
'error',
interaction,
this,
interactionCommand,
err
);
} catch (errhook) {
this._logger.error(
`Command interaction run hook: error: ${interactionCommand.name} - ${errhook.message} - ${errhook.stack}`
);
this.emit(
'aghanim:command-interaction:error',
errhook,
interaction,
this,
interactionCommand
);
}
}
}
}
handleEvent(eventname) {
return (...args) => {
Object.keys(this.components)
.map((componentName) => this.components[componentName])
.filter((component) => component[eventname] && component.enable)
.forEach(async (component) => {
try {
await component[eventname](...args, this);
} catch (err) {
this._logger.error(
`${component.constructor.name} (${eventname}) - ${err}`
);
/**
* Fired when a component get an error to be executed
* @event Client#aghanim:component:error
* @param {object} err - Error
* @param {string} eventname - Name of Eris event
* @param {Client} client - Client instance
* @param {Component} component - Component
*/
this.emit(
'aghanim:component:error',
err,
eventname,
this,
component
);
}
});
};
}
/**
* Extend default args object.
* @param {args} args - Args object.
* @param {msg} msg - Eris Message.
* @param {Client} client - Client instance.
*/
extendCommandArgs(
args,
msg,
client
) {} /* eslint class-methods-use-this: "off" */
_addFromDirectory(dirname, func) {
if (!dirname.endsWith('/')) dirname += '/';
const pattern = `${dirname}*.js`;
const filenames = glob.sync(pattern);
filenames.forEach((filename) => func(filename));
}
/**
* Register a command to the client.
* @param {Command | object} command - The command to add to the bot.
* @returns {Command} - Command added
*/
addCommand(command) {
/* eslint consistent-return:"off" */
if (!(command instanceof Command) && typeof command === 'object') {
// allow command as object and create it
command = new Command(command);
}
if (!(command instanceof Command)) throw new TypeError('Not a command'); // throw error if not a Command instance or class extending of command
if (!this.categories.find((c) => c.name === command.category)) {
// Check category exists or assing default category
command.category = DEFAULT_CATEGORY;
this._logger.warn(
`Category not found for ${command.name}. Established as ${DEFAULT_CATEGORY}`
);
}
command.client = this; // inject client on command
const { requirements } = command;
command.requirements = []; // reset command.requirements
mapCommandRequirement(
this,
command,
requirements
); /* eslint no-use-before-define: "off" */
// Check if command exists already and throw error or add to client
if (!command.childOf) {
const commandExists = this.commands.find((c) =>
c.names.some((cname) =>
[command.name, ...command.aliases].includes(cname)
)
);
if (commandExists) {
this._logger.error(`Command exists: ${command.name}`);
} else {
this.commands.push(command);
this._logger.debug(`Command added: ${command.name}`);
return command;
}
} else {
// Find parent command and add to client
const parent = this.commands.find((c) =>
c.names.includes(command.childOf)
);
if (!parent) {
throw new Error(
`Parent command ${command.childOf} not found for ${command.name}`
);
} else {
if (command.category !== parent.category) {
// Set category as parent category if is different
command.category = parent.category;
this._logger.warn(
`${command.category} not same upcomand! Established as ${parent.category}`
);
}
command.parent = parent;
parent.childs.push(command);
this._logger.debug(
`Subcommand added: ${command.name} from ${parent.name}`
);
return command;
}
}
}
/**
* Load all the JS files in a directory and attempt to load them each as commands.
* @param {string} dirname - The location of the directory.
*/
addCommandDir(dirname) {
this._addFromDirectory(dirname, (filename) =>
this.addCommandFile(filename)
);
}
/**
* Load a command exported from a file.
* @param {string} filename - The location of the file.
* @returns {Command} - Command added.
*/
addCommandFile(filename) {
try {
const commandLoaded = reload(filename);
const command = this.addCommand(commandLoaded);
if (command) {
command.filename = filename;
}
return command;
} catch (err) {
this._logger.error(`${err.stack} on ${filename}`);
}
}
/**
* Add a Command {@link Category}
* @param {string} name - Name for Category
* @param {string} help - Help Message
* @param {object} options - Options
* @param {object} options.hide - Options
* @param {object} options.restrict - Options
*/
addCategory(name, help, options) {
const category = new Category(name, help, options);
if (this.categories.find((c) => c.name === category.name)) {
this._logger.error(`${category.name} exists`);
} else {
this.categories.push(category);
this._logger.debug(`Category added: ${category.name}`);
}
}
/**
* Add a Component
* @param {Component | object} component - Component {@link Component}
* @returns {Component} - Component added
*/
addComponent(component, options) {
if (!(component instanceof Component) && typeof component === 'object') {
// allow load components as object
const componentObject = component;
if (!componentObject.name)
throw new TypeError(
`Component as object require an name => ${JSON.stringify(
componentObject
)}`
);
component = class extends Component {
constructor(client, options) {
super(client, options);
if (typeof componentObject.constructor === 'function') {
componentObject.constructor(client, options);
}
}
};
Object.defineProperty(component, 'name', { value: componentObject.name });
Object.keys(componentObject)
.filter((key) => key !== 'name')
.forEach((key) => (component.prototype[key] = componentObject[key]));
}
if (!(component.prototype instanceof Component))
throw new TypeError(`Not a Component => ${component}`);
if (this.components[component.name]) {
throw new Error(`Component exists => ${component.name}`);
}
try {
const instanceComponent = new component(
this,
options
); /* eslint new-cap: "off" */
if (instanceComponent.enable) {
instanceComponent.name = component.name;
this.components[component.name] = instanceComponent;
this._logger.debug(`Component Added: ${component.name}`);
return this.components[component.name];
} else {
this._logger.warn(`Component Disabled: ${component.name}`);
}
} catch (err) {
this._logger.error(`${component.name} - ${err}`);
}
}
/**
* Add component from file
* @param {string} filename Path to file
* @returns {Component} Component added
*/
addComponentFile(filename) {
try {
const componentClass = reload(filename);
const component = this.addComponent(componentClass);
if (component) {
component.filename = filename;
}
return component;
} catch (err) {
this._logger.error(`${err} on ${filename}`);
}
}
/**
* Add components from a directory
* @param {string} dirname Path to load components
*/
addComponentDir(dirname) {
this._addFromDirectory(dirname, (filename) =>
this.addComponentFile(filename)
);
}
/**
* Define a requirement that can be added by commands
* @param {(CommandRequirementObject|CommandRequirementFunction)} requirement - Requirement to define
*/
addCommandRequirement(requirement) {
if (typeof requirement === 'object' && requirement.type) {
this._commandsRequirements[requirement.type] = requirement;
return requirement;
} else if (typeof requirement === 'function') {
this._commandsRequirements[requirement.name] = requirement;
return requirement;
} else {
this._logger.error('Error adding command requirement');
}
}
/**
* Add command requirement from file
* @param {string} filename Path to file
* @returns {CommandRequirement} CommandRequirement added
*/
addCommandRequirementFile(filename) {
try {
const requirementLoaded = reload(filename);
if (typeof requirementLoaded === 'function')
Object.defineProperty(requirementLoaded, 'name', {
value: path.basename(filename, '.js')
});
const requirement = this.addCommandRequirement(requirementLoaded);
if (requirement) {
requirement.filename = filename;
}
return requirement;
} catch (err) {
this._logger.error(`${err} on ${filename}`);
}
}
/**
* Add command requirements from a directory
* @param {string} dirname Path to load command requirements
*/
addCommandRequirementDir(dirname) {
this._addFromDirectory(dirname, (filename) =>
this.addCommandRequirementFile(filename)
);
}
/**
* Reloads all commands that were loaded via `addCommandFile` and
* `addCommandDir`. Useful for development to hot-reload commands as you work
* on them.
*/
reloadCommands() {
this._logger.debug('Reloading commands...');
const commands = this.commands.reduce((filenames, command) => {
filenames.push(command.filename ? command.filename : command);
if (command.childs.length > 0) {
command.childs.forEach((subcommand) => {
filenames.push(
subcommand.filename ? subcommand.filename : subcommand
);
});
}
return filenames;
}, []);
this.commands = [];
commands.forEach((command) =>
typeof command === 'string'
? this.addCommandFile(command)
: this.addCommand(command)
); /* eslint no-confusing-arrow: 'off' */
}
/**
* Reloads all components that were loaded via `addComponentFile` and
* `addCommponentDir`. Useful for development to hot-reload commands as you work
* on them.
*/
reloadComponents() {
this._logger.debug('Reloading components...');
const components = Object.keys(this.components)
.map((key) => this.components[key])
.reduce((filenames, component) => {
if (component.filename) {
filenames.push([
component.filename,
component.name || component.constructor.name
]);
}
return filenames;
}, []);
components.forEach(([filename, name]) => {
delete this.components[name];
this.addComponentFile(filename);
});
this.handleEvent('ready')();
}
addInteractionCommand(command) {
if (!(command instanceof Command) && typeof command === 'object') {
// allow command as object and create it
command = new Command(command);
}
const { requirements = [] } = command;
command.requirements = requirements.map((requirement) => {
const resolvedRequirement = getCommandRequirement(
this,
command,
requirement
);
return resolvedRequirement;
});
mapCommandRequirement(
this,
command,
requirements
); /* eslint no-use-before-define: "off" */
// reqs.forEach(req => {
// const requirement = getCommandRequirement(client, command, req)
// if(Array.isArray(requirement)){
// mapCommandRequirement(client, command, requirement)
// }else{
// command.addRequirement(requirement)
// }
// })
this.interactionCommands.push(command);
}
/**
* Load all the JS files in a directory and attempt to load them each as commands.
* @param {string} dirname - The location of the directory.
*/
addInteractionCommandDir(dirname) {
this._addFromDirectory(dirname, (filename) =>
this.addInteractionCommandFile(filename)
);
}
/**
* Load a command exported from a file.
* @param {string} filename - The location of the file.
* @returns {Command} - Command added.
*/
addInteractionCommandFile(filename) {
try {
const commandLoaded = reload(filename);
const command = this.addInteractionCommand(commandLoaded);
if (command) {
command.filename = filename;
}
return command;
} catch (err) {
this._logger.error(`${err.stack} on ${filename}`);
}
}
reloadCommandRequirements() {
this._logger.debug('Reloading command requirements...');
const filenamesRequirement = Object.keys(this._commandsRequirements)
.map((key) => this._commandsRequirements[key])
.reduce((filenames, requirement) => {
if (requirement.filename) {
filenames.push(requirement.filename);
}
return filenames;
}, []);
// this._commandsRequirements = {}
filenamesRequirement.forEach((filename) =>
this.addCommandRequirementFile(filename)
);
}
/**
* Checks the list of registered commands and returns one whch is known by a
* given name, either as the command's name or an alias of the command.
* @param {string} command - The name of the command to look for.
* @param {string} subcommand - The name of the subcommand to look for.
* @return {Command|undefined}
*/
getCommandByName(command, subcommand) {
const cmd = this.commands.find((c) => c.names.includes(command));
if (!cmd) return;
if (subcommand) {
const scmd = cmd.childs.find((c) => c.names.includes(subcommand));
return scmd || cmd;
}
return cmd;
}
/***
* Returns the appropriate prefix string to use for commands based on a
* certain message.
* @param {Object} msg - The message to check the prefix of.
* @return {string}
*/
getPrefixForMessage(msg) {
return this.prefix;
}
/***
* Takes a message, gets the prefix based on the config of any guild it was
* sent in, and returns the message's content without the prefix if the
* prefix matches, and `null` if it doesn't.
* @param {Object} msg - The message to process
* @return {Array<String|null>}
**/
splitPrefixFromContent(msg) {
// Traditional prefix handling - if there is no prefix, skip this rule
const prefix = this.getPrefixForMessage(msg); // TODO: guild config
if (prefix !== undefined && msg.content.startsWith(prefix)) {
return { prefix, content: msg.content.substr(prefix.length) };
}
// Allow mentions to be used as prefixes according to config
const match = msg.content.match(this.mentionPrefixRegExp);
if (this.allowMention && match) {
// TODO: guild config
return { prefix: match[0], content: msg.content.substr(match[0].length) };
}
// we got nothing
return { prefix: undefined, content: msg.content };
}
/**
* Get Commands from a Category
* @param {(string|string[])} categories - Category or list of these to search commands
* @return {Command[]|undefined} - Array of {@link Command} (include childs commands aka subcommands)
*/
getCommandsOfCategories(categories) {
if (!Array.isArray(categories)) {
categories = [categories];
}
categories = categories.map((c) => c.toLowerCase());
const cmds = this.commands.filter((c) =>
categories.includes(c.category.toLowerCase())
);
return cmds.length > 0 ? cmds : undefined;
}
/**
* If returns true, allow default commands management and messageCreate Components functions.
* @param {Eris.message} msg - Eris Message object
* @param {Client} client - Client instance
* @return {boolean} - true = allow, false = omit
*/
triggerMessageCreate(msg, client) {
return true;
}
/**
* @typedef EmbedMessageObject
* @see {@link https://abal.moe/Eris/docs/TextChannel#function-createMessage EmbedMessageObject}
*/
/**
* Creators for Command Requirements
* @typedef {function} CommandRequirementsCreators
* @param {object} config - Object with requirement config
* @see {@link https://desvelao.github.io/aghanim/tutorial-6command-requirements.html Command Requirements Creators} config - Object with requirement config
* @returns {CommandRequirementObject}
*/
/**
* Create args object and find command. Returns both.
* @typedef parseCommand
* @prop {args} args - args object
* @prop {Command|undefined} command - command
*/
/**
* Create args object and find command. Returns both.
* @param {Eris.message} msg - Eris Message object
* @returns {parseCommand} -
*/
createCommandArgs(msg) {
const { prefix, content } = this.splitPrefixFromContent(msg);
if (typeof prefix !== 'string' || typeof content !== 'string') return;
const args = content.split(' ').map((word) => word.trim());
/**
* Message is spit for spaces (' ')
* @typedef args
* @prop {string} prefix - Message prefix
* @prop {string} content - Message content
* @prop {function} from - Splice message content from argument number to end message. fn(arg:number)
* @prop {function} until - Splice message content from begin until argument number. fn(arg:number)
* @prop {function} after - Same content. fn()
* @prop {Client} client - Client instance
* @prop {string} command - Command name
* @prop {(string|undefined)} subcommand - Subcommand name if exists
* @prop {array} - Each word form message is in a slot
*/
args.prefix = prefix;
args.content = content;
args.from = (arg) => args.slice(arg).join(' ');
args.until = (arg) => args.prefix + args.slice(0, arg).join(' ');
args.after = args.from(1);
args.client = this;
args.command = args[0];
args.subcommand = args[1];
this.extendCommandArgs(args, msg, this);
return args;
}
async checkRequirements(msg, args, client, command) {
if (!command.enable) {
return false;
}
return command.requirements.reduce(async (result, requirement) => {
if (!(await result)) {
return Promise.resolve(false);
}
if (typeof requirement === 'object') {
const pass = await requirement.validate(
msg,
args,
client,
command,
requirement
);
if (pass === null) {
// ignore response/responseDM/run methods
return Promise.resolve(false);
} else if (!pass) {
// false/undefined do response/responseDM/run methods
if (['string', 'object'].includes(typeof requirement.response)) {
await this.createMessage(msg.channel.id, requirement.response); // Response to message
} else if (typeof requirement.response === 'function') {
const res = await requirement.response(
msg,
args,
client,
command,
requirement
);
await this.createMessage(msg.channel.id, res); // Response to message
} else if (
['string', 'object'].includes(typeof requirement.responseDM)
) {
await msg.author
.getDMChannel()
.then((channel) => channel.createMessage(requirement.responseDM)); // Response with a dm
} else if (typeof requirement.responseDM === 'function') {
const res = await requirement.responseDM(
msg,
args,
client,
command,
requirement
);
await msg.author
.getDMChannel()
.then((channel) => channel.createMessage(res)); // Response with a dm
} else if (typeof requirement.run === 'function') {
await requirement.run(msg, args, client, command, requirement); // Custom
}
return Promise.resolve(false);
}
}
return Promise.resolve(true); // result
}, Promise.resolve(true));
}
}
function getCommandRequirement(client, command, req) {
if (typeof req === 'string') {
if (builtinCommandRequirements[req]) {
const requirement = builtinCommandRequirements[req]({ command, client });
requirement.type = req;
return requirement;
} else if (client._commandsRequirements[req]) {
if (typeof client._commandsRequirements[req] === 'object') {
return client._commandsRequirements[req];
} else if (typeof client._commandsRequirements[req] === 'function') {
return client._commandsRequirements[req]({ command, client });
}
} else {
throw new Error(`String command requirement not found: ${req}`);
}
} else if (typeof req === 'object') {
if (builtinCommandRequirements[req.type]) {
const requirement = builtinCommandRequirements[req.type]({
...req,
command,
client
});
requirement.type = req.type;
return requirement;
} else {
return req;
}
} else {
throw new TypeError(`Requirement: ${req} on ${command.name}`);
}
}
function mapCommandRequirement(client, command, reqs) {
reqs.forEach((req) => {
const requirement = getCommandRequirement(client, command, req);
if (Array.isArray(requirement)) {
mapCommandRequirement(client, command, requirement);
} else {
command.addRequirement(requirement);
}
});
}
module.exports = Client;