UNPKG

axoncore

Version:

The best fully featured discord bot framework. Universal Client, Command and Event handler.

718 lines (633 loc) 23.9 kB
// Lib - Modules import { EventEmitter } from 'events'; import util from 'util'; // Core - Core import Base from './Core/Base'; import EventManager from './Core/Event/EventManager'; import CommandDispatcher from './Core/CommandDispatcher'; // Registries import ModuleRegistry from './Core/Stores/ModuleRegistry'; import CommandRegistry from './Core/Stores/CommandRegistry'; import ListenerRegistry from './Core/Stores/ListenerRegistry'; import GuildConfigCache from './Core/Stores/GuildConfigCache'; import MessageManager from './Langs/MessageManager'; import ModuleLoader from './Core/Loaders/ModuleLoader'; import ClientInitialiser from './Core/Loaders/ClientInitialiser'; import Executor from './Core/Executor'; // Utility import AxonUtils from './Utility/AxonUtils'; import ALogger from './Loggers/ALogger'; // Selector import LibrarySelector from './Libraries/index'; import LoggerSelector from './Loggers/index'; // Misc import logo from './Configs/logo'; import packageJSON from '../package.json'; import { EMBED_LIMITS } from './Utility/Constants/DiscordEnums'; import { WEBHOOK_TYPES, LOG_LEVELS, WEBHOOK_TO_COLOR, DEBUG_FLAGS } from './Utility/Constants/AxonEnums'; /** * @typedef {import('./AxonOptions').default} AxonOptions * @typedef {import('./Core/Module').default} Module * @typedef {import('./Core/Event/AHandler').default} AHandler * @typedef {import('./Utility/Collection').default<AHandler>} HandlerCollection * @typedef {import('./Core/Event/Listener').default} Listener * @typedef {import('./Libraries/definitions/Resolver').default} Resolver * @typedef {import('./Core/Command/Command').default} Command * @typedef {import('./Loggers/Context').default} Context * @typedef {import('./Core/Models/GuildConfig').default} GuildConfig * @typedef {import('./Core/Command/CommandEnvironment').default} CommandEnvironment * @typedef {import(./Libraries/definitions/LibraryInterface).default} LibraryInterface * @typedef {import('./Core/Models/AxonConfig').default} AxonConfig * @typedef {{ * Utils: Utils, DBProvider: ADBProvider, AxonConfig: AxonConfig, GuildConfig: GuildConfig, DBLocation: String * }} Extensions */ /** * AxonCore - Client constructor * * @author KhaaZ * * @class AxonClient * @extends EventEmitter * * @prop {BotClient} _botClient - Discord library Client * @prop {ModuleRegistry} moduleRegistry - Registry holding all modules * @prop {CommandRegistry} commandRegistry - Registry holding all commands * @prop {ListenerRegistry} listenerRegistry - Registry holding all listeners * @prop {EventManager} eventManager - The EventManager instance that handle all AxonCore listeners * @prop {GuildConfigCache} guildConfigs - The Manager that handles GuildConfigs (cache / DB etc) * @prop {AxonConfig} [axonConfig] - The AxonConfig object that handles globally blacklisted users and guilds * @prop {CommandDispatcher} dispatcher - Dispatch commands onMessageCreate. * @prop {Executor} executor - Executor class that handles executing commands and listeners * @prop {ModuleLoader} moduleLoader - Load, unload modules. * @prop {MessageManager} _messageManager - Message manager object accessible with `<AxonClient>.l` * @prop {LibraryInterface} library - LibraryInterface object depending the lib used * @prop {ALogger} logger - The Logger instance * @prop {AxonUtils} axonUtils - Util methods (AxonCore) * @prop {Utils} utils - Utils methods (general) * @prop {ADBProvider} DBProvider - The DBProvider instance * @prop {Extensions} extensions - AxonCore extensions * @prop {Object} _configs - configs (webhooks, template, custom) * @prop {Object.<string, {id: String, token: String}>} _configs.webhooks - Webhooks configs with all webhooks id and tokens * @prop {{ embeds: Object.<string, Number>, emotes: Object.<string, String> }} _configs.template - Template config * @prop {AxonOptions} _configs.custom - Custom config object optionally passed via AxonOptions * @prop {Object} staff - Bot Staff (owners, admins, +...) * @prop {Array<String>} staff.owners - Array of user IDs with BotOwner permissions * @prop {Array<String>} staff.admins - Array of user IDs with BotAdmin permissions * @prop {Object} settings - Bot settings * @prop {Boolean} settings.debugMode - Enable to show commands latency and debug informations * @prop {Array<String>} settings.prefixes - Default bot prefixes * @prop {String} settings.adminPrefix - Admins prefix : override perms/cd except Owner * @prop {String} settings.ownerPrefix - Owner prefix : override perms/cd * @prop {String} settings.lang - Default lang for the bot * @prop {Number} settings.guildConfigCache - Max amount of guildConfigs cached at the same time (LRUCache) * @prop {Object} info - General info about the current application * @prop {String} info.name - Bot name * @prop {String} info.description - Bot description * @prop {String} info.version - Bot version * @prop {Array<String>} info.owners - Bot owners (array of names) * @prop {Object} axoncore - AxonCore info * @prop {String} axoncore.version - AxonCore version * @prop {String} axoncore.author - AxonCore author * @prop {String} axoncore.github - AxonCore github link */ class AxonClient extends EventEmitter { /** * Creates an AxonClient instance. * * @param {BotClient} botClient - Eris or Discordjs Client instance * @param {AxonOptions} [axonOptions={}] - Axon options * @param {Object.<string, Module>} [modules={}] - Object with all modules to add in the bot * @memberof AxonClient */ constructor(botClient, axonOptions = {}, modules = {} ) { super(); axonOptions.logo ? axonOptions.logo(packageJSON.version) : logo(packageJSON.version); this._configs = { webhooks: axonOptions.webhooks, template: axonOptions.template, custom: axonOptions.custom, }; /* Bot settings */ this.settings = { debugMode: axonOptions.settings.debugMode || false, prefixes: [axonOptions.prefixes.general], adminPrefix: axonOptions.prefixes.admin, // meant to be different prefix on all AxonClient instance (global override) ownerPrefix: axonOptions.prefixes.owner, // meant to be same prefix on all AxonClient instance (global override) lang: axonOptions.settings.lang, guildConfigCache: axonOptions.settings.guildConfigCache, }; /* Bot informations */ this.info = { name: axonOptions.info.name, description: axonOptions.info.description, version: axonOptions.info.version, owners: Object.values(axonOptions.staff.owners).map(o => o.name), }; /* Client specification */ /** * @type {{version: String, author: String, github: String}} */ this.axoncore = { version: packageJSON.version, author: packageJSON.author, github: packageJSON.link, }; /* Logger */ if (axonOptions.extensions.logger && axonOptions.extensions.logger instanceof ALogger) { this.logger = axonOptions.extensions.logger; } else { this.logger = LoggerSelector.select(axonOptions.settings); } this.extensions = ClientInitialiser.initExtensions(this, axonOptions); /* AxonUtils */ this.axonUtils = new AxonUtils(this); /* * Initialise Bot Client and LibraryInterface */ /** * @type {BotClient} */ this._botClient = botClient; this.library = LibrarySelector.select(this, axonOptions); /* Utils */ this.utils = new this.extensions.Utils(this); // eslint-disable-line new-cap /* ADBProvider */ this.DBProvider = new this.extensions.DBProvider(this); this.DBProvider.init(); this.log('NOTICE', `DB ready. [TYPE: ${this.DBProvider.type}]`); if (this.settings.debugMode) { this.on('debug', this.onDebug); } /* Core */ this.moduleRegistry = new ModuleRegistry(this); this.commandRegistry = new CommandRegistry(this); this.listenerRegistry = new ListenerRegistry(this); this.eventManager = new EventManager(this); /* GuildConfigs */ this.guildConfigs = new GuildConfigCache(this, axonOptions.settings.guildConfigCache); // Guild ID => guildConfig /* Core Logic */ this.moduleLoader = new ModuleLoader(this); this.dispatcher = new CommandDispatcher(this); this.executor = new Executor(this); this._messageManager = new MessageManager(this, axonOptions.lang, axonOptions.settings.lang); /* General */ this.staff = ClientInitialiser.initStaff(axonOptions.staff, this.logger); /* AxonConfig */ /** * @type {AxonConfig} */ this.axonConfig = null; ClientInitialiser.initAxon(this); /* Additional loading / properties */ this.onInit(); /* Load modules */ console.log(' '); this.moduleLoader.loadAll(modules || {} ); // load modules console.log(' '); } // **** GETTERS **** // /** * Returns the bot client instance * * @readonly * @type {BotClient} * @memberof AxonClient */ get botClient() { return this._botClient; } /** * Returns all event handlers in eventManager * * @readonly * @type {HandlerCollection} * @memberof AxonClient */ get handlers() { return this.eventManager.handlers; } /** * Returns all registered listeners for the discord event name * * @param {String} eventName * @returns {Array<Listener>} * @memberof AxonClient */ getListeners(eventName) { return this.eventManager.getListeners(eventName); } /** * Returns all the resolver for the default current library used. * Can be easily overridden with a custom Resolver by overriding this getter. * * @readonly * @type {Resolver} * @memberof AxonClient */ get Resolver() { return this.library.resolver; } /** * Return the MessageManager instance * * @readonly * @type {MessageManager} * @memberof AxonClient */ get l() { return this._messageManager; } /** * Return the webhooks config * * @readonly * @type {{ * FATAL: {id: String, token: String}, ERROR: {id: String, token: String}, WARN: {id: String, token: String}, DEBUG: {id: String, token: String}, * NOTICE: {id: String, token: String}, INFO: {id: String, token: String}, VERBOSE: {id: String, token: String} * }} * @memberof AxonClient */ get webhooks() { return this._configs.webhooks; } /** * Returns the template config * * @readonly * @type {{embeds: Object.<string, Number>, emotes: Object.<string, String>}} * @memberof AxonClient */ get template() { return this._configs.template; } /** * Returns the custom config * * @readonly * @type {Object.<string, any>} * @memberof AxonClient */ get custom() { return this._configs.custom; } /** * Get a module from AxonClient with the given label. * * @param {String} module - Module label * @returns {Module|null} * @memberof AxonClient */ getModule(module) { return this.moduleRegistry.get(module); } /** * Get a command/subcommand from AxonClient with the given full label. * * @param {String} fullLabel - Full command (or subcommand) label * @returns {Command|null} * @memberof AxonClient */ getCommand(fullLabel) { return this.commandRegistry.getFull(fullLabel.split(' ') ); } // **** MAIN **** // /** * Start AxonClient. * Start bot client. * Bind error listeners and event listeners. * * Calls custom onStart() method at the beginning. * Calls custom onReady() method when AxonClient is ready. * * @async * @memberof AxonClient */ async start() { await this.onStart(); this.library.client.connect() .then( () => { this.log('NOTICE', '=== BotClient Connected! ==='); } ) .catch(err => { this.logger.error(err.stack); } ); try { /* Init Error listeners */ this.initErrorListeners(); /* Bind Listeners to Handlers */ this.eventManager.bindListeners(); this.log('NOTICE', '=== AxonClient Ready! ==='); /* Custom onReady method */ this.onReady(); } catch (err) { this.log('FATAL', err); } this.library.onMessageCreate(this._onMessageCreate.bind(this) ); this.library.onceReady(this._onReady.bind(this) ); } // **** LifeCycle methods **** // /** * Override this method. * Method executed after the object is finished to be constructed (in the constructor) * * @returns {Boolean} * @memberof AxonClient */ onInit() { return true; } /** * Override this method. * Method executed at the beginning of the start method. * * @returns {Promise<Boolean>} * @memberof AxonClient */ onStart() { return Promise.resolve(true); } /** * Override this method. * Method executed at the end of the start method (when the AxonClient is ready). * * @returns {Promise<Boolean>} * @memberof AxonClient */ onReady() { return Promise.resolve(true); } /** * Log both to console and to the correct webhook * * @param {LOG_LEVELS} level - The LOG-LEVEL * @param {String|Error} content - The content or the error to log * @param {Context} [ctx=null] - Additional context to be passed to logger * @param {Boolean} [execWebhook=true] - Whether to execute the webhook * @memberof AxonClient */ log(level, content, ctx = null, execWebhook = true) { if (!LOG_LEVELS[level] ) { this.logger.warn(`Incorrect log level: ${level}`); } let err = null; if (content instanceof Error) { err = (content.stack && content.stack.length < EMBED_LIMITS.LIMIT_DESCRIPTION) ? content.stack : content.message; content = content.stack || content.message; } this.logger[LOG_LEVELS[level]](content, ctx); if (content.length > EMBED_LIMITS.LIMIT_DESCRIPTION) { content = content.slice(0, EMBED_LIMITS.LIMIT_DESCRIPTION); } if (execWebhook && this.library) { const whType = WEBHOOK_TYPES[level]; this.axonUtils.triggerWebhook(whType, { color: WEBHOOK_TO_COLOR[whType], timestamp: new Date(), description: err || content, // eslint-disable-next-line no-nested-ternary }, `${whType}${this.library.client.getUser() ? ` - ${this.library.client.getUsername()}` : this.info.name ? ` - ${this.info.name}` : ''}`); } } /** * Function executed on the global messageCreate event and dispatch to the correct command and execution * * @param {Message} msg * @memberof AxonClient */ _onMessageCreate(msg) { if (!this.botClient.ready) { return; } /* msg.author error + ignore self + ignore bots */ if (!this.library.message.getAuthor(msg) || this.library.user.isBot(msg.author) ) { return; } this.dispatcher.dispatch(msg); } /** * Function executed when the bot client is ready. * Bind events and initialise client status/game. * @memberof AxonClient */ _onReady() { this.log('NOTICE', '=== BotClient Ready! ==='); this.botClient.ready = true; /* Bind handlers to events */ this.eventManager.bindHandlers(); /* Initialise status. Default AxonCore status or use custom one */ this.initStatus(); this.log('NOTICE', '=== **Ready!** ==='); } /** * Function ran on debug event. * Logs the debug event. * * @param {DEBUG_FLAGS} flag * @param {String} d * @memberof AxonClient */ onDebug(flag, d) { let m = ''; if (flag & DEBUG_FLAGS.GOOD) { m += 'V: '; } else if (flag & DEBUG_FLAGS.BAD) { m += 'X: '; } if (flag & DEBUG_FLAGS.INIT) { m += '[INIT] '; } else if (flag & DEBUG_FLAGS.COMMAND) { m += '[CMD] '; } this.logger.verbose(`${m}${d}`); } /** * Initialize error listeners and webhooks. * Override this method to setup your own error listeners. * @memberof AxonClient */ initErrorListeners() { process.on('uncaughtException', (err) => { this.log('FATAL', err); } ); process.on('unhandledRejection', (err) => { this.log('FATAL', err); } ); this.botClient.on('error', (err) => { this.log('ERROR', err); } ); this.botClient.on('warn', (msg) => { this.log('ERROR', msg); } ); this.log('INFO', 'Error listeners bound!'); } /** * Set the bot status. Override to setup your own status. * Called after the client ready event. * @memberof AxonClient */ initStatus() { this.library.client.setPresence('online', { name: `AxonCore | ${this.settings.prefixes[0]}help`, type: 0, } ); } // **** HELPERS **** // /** * Send full help in DM. * Doesn't show commands that the user can't execute. * This method can be overridden in child. * * @param {Message} msg - The message object * @param {GuildConfig} guildConfig * * @memberof AxonClient */ async sendFullHelp(msg, guildConfig) { const prefix = (guildConfig && guildConfig.getPrefixes().length > 0) ? guildConfig.getPrefixes()[0] : this.settings.prefixes[0]; const embed = {}; embed.author = { name: `Help for ${this.library.client.getUsername()}`, icon_url: this.library.client.getAvatar(), }; embed.description = this.info.description; embed.footer = { text: 'Runs with AxonCore', }; embed.color = typeof this.template.embeds.help === 'string' ? parseInt(this.template.embeds.help, 16) || null : this.template.embeds.help; let commandList = ''; if (guildConfig) { for (const module of this.moduleRegistry.registry.values() ) { const commands = module.commands.filter(c => c.permissions.canExecute(msg, guildConfig)[0] ); if (commands.length > 0) { commandList += `**${module.label}**\n${commands.map(c => `\`${prefix}${c.label}\` - ${c.info.description}`).join('\n')}\n`; } } } else { for (const module of this.moduleRegistry.registry.values() ) { commandList += `**${module.label}**\n${module.commands.map(c => `\`${prefix}${c.label}\` - ${c.info.description}`).join('\n')}\n`; } } try { const chan = await this.library.user.getDM(this.library.message.getAuthor(msg) ); /* Split commandList */ // eslint-disable-next-line no-magic-numbers if (commandList.length > 1800) { commandList = commandList.match(/[\s\S]{1,1800}[\n\r]/g) || []; for (const match of commandList) { embed.description = match; await this.library.channel.sendMessage(chan, { embed } ); } } else { embed.description = commandList; await this.library.channel.sendMessage(chan, { embed } ); } } catch (err) { this.logger.verbose(err); } } /** * Register a guild prefix. * Shortcut to guildConfig.registerPrefix() * * @param {String} gID - The guild ID * @param {Array<String>} prefixArr - The array of prefixes * @returns {Promise<GuildConfig>} The guild Schema from the DB / Error if error * * @memberof AxonClient */ async registerGuildPrefixes(gID, prefixArr) { const guildConfig = await this.guildConfigs.getOrFetch(gID); return guildConfig.updatePrefixes(prefixArr); } // ***** GENERAL **** // /** * Custom toString method. * * @returns {String} * @memberof AxonClient */ toString() { return this.constructor.name; } /** * Custom ToJSON method. * (Based of Eris') * * @returns {Object} JSON-like Object * @memberof AxonClient */ toJSON() { return Base.prototype.toJSON.call(this); } /** * Custom inspect method * Doesn't list prefixed property and undefined property. * (Based of Eris') * * @returns {Object} Object to inspect * @memberof AxonClient */ [util.inspect.custom]() { return Base.prototype[util.inspect.custom].call(this); } } /** * Fired when a debug message needs to be sent * @event AxonClient#debug * @prop {DEBUG_FLAGS} flags - Debug flags used to have more information about the event * @prop {String} debugMessage - Debug message with information about the situation * @memberof AxonClient */ /** * Fired when a command is successfully ran * @event AxonClient#commandExecution * @prop {Boolean} status - If the command was successfully executed or not * @prop {String} commandFullLabel - The command fullLabel * @prop {Object} data * @prop {Message} data.msg - The message that triggered the command * @prop {Command} data.command - The Command that was executed * @prop {GuildConfig} data.guildConfig - The GuildConfig * @prop {CommandContext} data.context - The execution context * @memberof AxonClient */ /** * Fired when a command fails * @event AxonClient#commandError * @prop {String} commandFullLabel - The command fullLabel * @prop {Object} data * @prop {Message} data.msg - The message that triggered the command * @prop {Command} data.command - The Command that was executed * @prop {GuildConfig} data.guildConfig - The GuildConfig * @prop {AxonCommandError} data.error - The error * @memberof AxonClient */ /** * Fired when a listener is executed * @event AxonClient#listenerExecution * @prop {Boolean} status - Whether the listener was successfully executed or not * @prop {String} eventName - The discord event name * @prop {String} listenerName - The listener label * @prop {Object} data - Additional information * @prop {Listener} data.listener - The Listener that was executed * @prop {GuildConfig} data.guildConfig - The GuildConfig object * @memberof AxonClient */ /** * Fired when a listener errors * @event AxonClient#listenerError * @prop {String} eventName - The discord event name * @prop {String} listenerName - The Listener label * @prop {Object} data - Additional information * @prop {Listener} data.listener - The Listener that was executed * @prop {GuildConfig} data.guildConfig - The GuildConfig object * @prop {Error} data.error - The error * @memberof AxonClient */ export default AxonClient;