discord-akairo
Version:
A highly customizable bot framework for Discord.js.
289 lines (246 loc) • 8.61 kB
JavaScript
const AkairoError = require('../util/AkairoError');
const { AkairoHandlerEvents } = require('../util/Constants');
const AkairoModule = require('./AkairoModule');
const Category = require('../util/Category');
const { Collection } = require('discord.js');
const EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
/**
* Base class for handling modules.
* @param {AkairoClient} client - The Akairo client.
* @param {AkairoHandlerOptions} options - Options for module loading and handling.
* @extends {EventEmitter}
*/
class AkairoHandler extends EventEmitter {
constructor(client, {
directory,
classToHandle = AkairoModule,
extensions = ['.js', '.json', '.ts'],
automateCategories = false,
loadFilter = (() => true)
}) {
super();
/**
* The Akairo client.
* @type {AkairoClient}
*/
this.client = client;
/**
* The main directory to modules.
* @type {string}
*/
this.directory = directory;
/**
* Class to handle.
* @type {Function}
*/
this.classToHandle = classToHandle;
/**
* File extensions to load.
* @type {Set<string>}
*/
this.extensions = new Set(extensions);
/**
* Whether or not to automate category names.
* @type {boolean}
*/
this.automateCategories = Boolean(automateCategories);
/**
* Function that filters files when loading.
* @type {LoadPredicate}
*/
this.loadFilter = loadFilter;
/**
* Modules loaded, mapped by ID to AkairoModule.
* @type {Collection<string, AkairoModule>}
*/
this.modules = new Collection();
/**
* Categories, mapped by ID to Category.
* @type {Collection<string, Category>}
*/
this.categories = new Collection();
}
/**
* Registers a module.
* @param {AkairoModule} mod - Module to use.
* @param {string} [filepath] - Filepath of module.
* @returns {void}
*/
register(mod, filepath) {
mod.filepath = filepath;
mod.client = this.client;
mod.handler = this;
this.modules.set(mod.id, mod);
if (mod.categoryID === 'default' && this.automateCategories) {
const dirs = path.dirname(filepath).split(path.sep);
mod.categoryID = dirs[dirs.length - 1];
}
if (!this.categories.has(mod.categoryID)) {
this.categories.set(mod.categoryID, new Category(mod.categoryID));
}
const category = this.categories.get(mod.categoryID);
mod.category = category;
category.set(mod.id, mod);
}
/**
* Deregisters a module.
* @param {AkairoModule} mod - Module to use.
* @returns {void}
*/
deregister(mod) {
if (mod.filepath) delete require.cache[require.resolve(mod.filepath)];
this.modules.delete(mod.id);
mod.category.delete(mod.id);
}
/**
* Loads a module, can be a module class or a filepath.
* @param {string|Function} thing - Module class or path to module.
* @param {boolean} [isReload=false] - Whether this is a reload or not.
* @returns {AkairoModule}
*/
load(thing, isReload = false) {
const isClass = typeof thing === 'function';
if (!isClass && !this.extensions.has(path.extname(thing))) return undefined;
let mod = isClass
? thing
: function findExport(m) {
if (!m) return null;
if (m.prototype instanceof this.classToHandle) return m;
return m.default ? findExport.call(this, m.default) : null;
}.call(this, require(thing));
if (mod && mod.prototype instanceof this.classToHandle) {
mod = new mod(this); // eslint-disable-line new-cap
} else {
if (!isClass) delete require.cache[require.resolve(thing)];
return undefined;
}
if (this.modules.has(mod.id)) throw new AkairoError('ALREADY_LOADED', this.classToHandle.name, mod.id);
this.register(mod, isClass ? null : thing);
this.emit(AkairoHandlerEvents.LOAD, mod, isReload);
return mod;
}
/**
* Reads all modules from a directory and loads them.
* @param {string} [directory] - Directory to load from.
* Defaults to the directory passed in the constructor.
* @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded.
* Defaults to the filter passed in the constructor.
* @returns {AkairoHandler}
*/
loadAll(directory = this.directory, filter = this.loadFilter || (() => true)) {
const filepaths = this.constructor.readdirRecursive(directory);
for (let filepath of filepaths) {
filepath = path.resolve(filepath);
if (filter(filepath)) this.load(filepath);
}
return this;
}
/**
* Removes a module.
* @param {string} id - ID of the module.
* @returns {AkairoModule}
*/
remove(id) {
const mod = this.modules.get(id.toString());
if (!mod) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
this.deregister(mod);
this.emit(AkairoHandlerEvents.REMOVE, mod);
return mod;
}
/**
* Removes all modules.
* @returns {AkairoHandler}
*/
removeAll() {
for (const m of Array.from(this.modules.values())) {
if (m.filepath) this.remove(m.id);
}
return this;
}
/**
* Reloads a module.
* @param {string} id - ID of the module.
* @returns {AkairoModule}
*/
reload(id) {
const mod = this.modules.get(id.toString());
if (!mod) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
if (!mod.filepath) throw new AkairoError('NOT_RELOADABLE', this.classToHandle.name, id);
this.deregister(mod);
const filepath = mod.filepath;
const newMod = this.load(filepath, true);
return newMod;
}
/**
* Reloads all modules.
* @returns {AkairoHandler}
*/
reloadAll() {
for (const m of Array.from(this.modules.values())) {
if (m.filepath) this.reload(m.id);
}
return this;
}
/**
* Finds a category by name.
* @param {string} name - Name to find with.
* @returns {Category}
*/
findCategory(name) {
return this.categories.find(category => {
return category.id.toLowerCase() === name.toLowerCase();
});
}
/**
* Reads files recursively from a directory.
* @param {string} directory - Directory to read.
* @returns {string[]}
*/
static readdirRecursive(directory) {
const result = [];
(function read(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filepath = path.join(dir, file);
if (fs.statSync(filepath).isDirectory()) {
read(filepath);
} else {
result.push(filepath);
}
}
}(directory));
return result;
}
}
module.exports = AkairoHandler;
/**
* Emitted when a module is loaded.
* @event AkairoHandler#load
* @param {AkairoModule} mod - Module loaded.
* @param {boolean} isReload - Whether or not this was a reload.
*/
/**
* Emitted when a module is removed.
* @event AkairoHandler#remove
* @param {AkairoModule} mod - Module removed.
*/
/**
* Options for module loading and handling.
* @typedef {Object} AkairoHandlerOptions
* @prop {string} [directory] - Directory to modules.
* @prop {Function} [classToHandle=AkairoModule] - Only classes that extends this class can be handled.
* @prop {string[]|Set<string>} [extensions] - File extensions to load.
* By default this is .js, .json, and .ts files.
* @prop {boolean} [automateCategories=false] - Whether or not to set each module's category to its parent directory name.
* @prop {LoadPredicate} [loadFilter] - Filter for files to be loaded.
* Can be set individually for each handler by overriding the `loadAll` method.
*/
/**
* Function for filtering files when loading.
* True means the file should be loaded.
* @typedef {Function} LoadPredicate
* @param {String} filepath - Filepath of file.
* @returns {boolean}
*/