diff --git a/mkdocs.yml b/mkdocs.yml index 791659b..902d399 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,7 +62,6 @@ markdown_extensions: permalink: true - footnotes - meta -# pymd - pymdownx.arithmatex - pymdownx.betterem: smart_enable: all diff --git a/src/index.js b/src/index.js index 7023855..0d4abd0 100644 --- a/src/index.js +++ b/src/index.js @@ -66,6 +66,7 @@ const log = new Logger({ const I18n = require('@eartharoid/i18n'); const { CommandManager } = require('./modules/commands'); +const { PluginManager } = require('./modules/plugins'); const { Client, @@ -84,20 +85,29 @@ class Bot extends Client { intents: Intents.NON_PRIVILEGED, } }); - - Object.assign(this, { - commands: new CommandManager(this), - config, - db: require('./database')(log), // this.db.models.Ticket... - log, - i18n: new I18n(path('./src/locales'), 'en-GB') - }); - + /** The global bot configuration */ + this.config= config; + /** A sequelize instance */ + this.db = require('./database')(log), // this.db.models.Ticket... + /** A leekslazylogger instance */ + this.log = log; + /** An @eartharoid/i18n instance */ + this.i18n = new I18n(path('./src/locales'), 'en-GB'); + + // set the max listeners for each event to the number in the config this.setMaxListeners(this.config.max_listeners); + // check for updates require('./updater')(this); + // load internal listeners require('./modules/listeners')(this); - require('./modules/plugins')(this); + + /** The command manager, used by internal and plugin commands */ + this.commands = new CommandManager(this); + /** The plugin manager */ + this.plugins = new PluginManager(this); + // load plugins + this.plugins.load(); this.log.info('Connecting to Discord API...'); diff --git a/src/modules/plugins.js b/src/modules/plugins.js deleted file mode 100644 index 0dc6bf6..0000000 --- a/src/modules/plugins.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = client => { - client.config.plugins.forEach(plugin => { - try { - let package = require(`${plugin}/package.json`); - client.log.plugins(`Loading ${package.name} v${package.version} by ${package.author?.name || 'unknown'}`); - require(plugin)(client); - } catch (e) { - client.log.warn(`An error occurred whilst loading ${plugin}, have you installed it?`); - client.log.error(e); - return process.exit(); - } - }); - -}; \ No newline at end of file diff --git a/src/modules/plugins/index.js b/src/modules/plugins/index.js new file mode 100644 index 0000000..8b43a1b --- /dev/null +++ b/src/modules/plugins/index.js @@ -0,0 +1,4 @@ +module.exports = { + PluginManager: require('./manager'), + Plugin: require('./plugin') +}; \ No newline at end of file diff --git a/src/modules/plugins/manager.js b/src/modules/plugins/manager.js new file mode 100644 index 0000000..4408b00 --- /dev/null +++ b/src/modules/plugins/manager.js @@ -0,0 +1,110 @@ +// eslint-disable-next-line no-unused-vars +const { Collection, Client } = require('discord.js'); +// eslint-disable-next-line no-unused-vars +const Plugin = require('./plugin'); + +const fs = require('fs'); +const { join } = require('path'); +const { path } = require('../../utils/fs'); + +/** Manages the loading of plugins */ +module.exports = class PluginManager { + /** + * Create a PluginManager instance + * @param {Client} client + */ + constructor(client) { + /** The Discord Client */ + this.client = client; + /** A discord.js Collection (Map) of loaded plugins */ + this.plugins = new Collection(); + + /** Array of official plugins to be used to check if a plugin is official */ + this.official = [ + 'dsctickets.portal' + ]; + } + + /** + * Register and load a plugin + * @param {Boolean} npm Installed by NPM? + * @param {Plugin} Main The Plugin class + * @param {Object} pkg Contents of package.json + */ + registerPlugin(npm, Main, pkg) { + let { + name: id, + version, + author, + description + } = pkg; + + if (this.plugins.has(id)) { + this.client.log.warn(`[PLUGINS] A plugin with the ID "${id}" is already loaded, skipping`); + return; + } + + if (typeof author === 'object') { + author = author.name || 'unknown'; + } + + let loading = npm ? 'Loading' : 'Sideloading'; + + this.client.log.plugins(`${loading} "${id}" v${version} by ${author}`); + + let about = { + id, + version, + author, + description + }; + + this.plugins.set(id, about); + + try { + let plugin = new Main(this.client, id); + plugin.load(); + + this.plugins.set(id, Object.assign(about, { + name: plugin.name || id, + })); + + } catch (e) { + if (npm) { + this.client.log.warn(`An error occurred whilst loading "${id}"`); + } else { + this.client.log.warn(`An error occurred whilst sideloading "${id}"; have you manually installed its dependencies?`); + } + this.client.log.error(e); + process.exit(); + } + } + + /** + * Automatically register and load plugins + */ + load() { + // normal plugins (NPM) + this.client.config.plugins.forEach(plugin => { + try { + let pkg = require(`${plugin}/package.json`); + let main = require(plugin); + this.registerPlugin(true, main, pkg); + } catch (e) { + this.client.log.warn(`An error occurred whilst loading ${plugin}; have you installed it?`); + this.client.log.error(e); + process.exit(); + } + }); + + // sideload plugins for development + const dirs = fs.readdirSync(path('./user/plugins')); + dirs.forEach(dir => { + if (!fs.existsSync(path(`./user/plugins/${dir}/package.json`))) return; + let pkg = require(`../../../user/plugins/${dir}/package.json`); + let main = require(join(`../../../user/plugins/${dir}/`, pkg.main)); + this.registerPlugin(false, main, pkg); + }); + } + +}; \ No newline at end of file diff --git a/src/modules/plugins/plugin.js b/src/modules/plugins/plugin.js new file mode 100644 index 0000000..dd77fca --- /dev/null +++ b/src/modules/plugins/plugin.js @@ -0,0 +1,88 @@ +/* eslint-disable no-unused-vars */ +const { Client } = require('discord.js'); + +const fs = require('fs'); +const { join } = require('path'); +const { path } = require('../../utils/fs'); + +/** A plugin */ +module.exports = class Plugin { + /** + * Create a new Plugin + * @param {Client} client The Discord Client + * @param {String} id The plugin ID + * @param {Object} options Plugin options + * @param {String} options.name A human-friendly name (can be different to the name in package.json) + */ + constructor(client, id, options) { + if (typeof options === 'object') { + /** The human-friendly name of the plugin */ + this.name = options.name || id; + } + + /** The Discord Client */ + this.client = client; + /** The PluginManager */ + this.manager = this.client.plugins; + + // Object.assign(this, this.manager.plugins.get(id)); + // make JSDoc happy + + let { + version, + author, + description + } = this.manager.plugins.get(id); + + /** The unique ID of the plugin (NPM package name) */ + this.id = id; + /** The version of the plugin (NPM package version) */ + this.version = version; + /** The plugin author's name (NPM package author) */ + this.author = author; + /** The plugin description (NPM package description) */ + this.description = description; + + this.directory = {}; + /** A cleaned version of the plugin's ID suitable for use in the directory name */ + this.directory.name = this.id.replace(/@[-_a-zA-Z0-9]+\//, ''); + /** The absolute path of the plugin directory */ + this.directory.path = path(`./user/plugins/${this.directory.name}`); + } + + /** + * Create the plugin directory if it doesn't already exist + * @returns {Boolean} True if created, false if it already existed + */ + createDirectory() { + if (!fs.existsSync(this.directory.path)) { + this.client.log.plugins(`Creating plugin directory for "${this.name}"`); + fs.mkdirSync(this.directory.path); + return true; + } else { + return false; + } + } + + /** + * Create the plugin config file if it doesn't already exist + * @param {Object} template The default config template + * @returns {Boolean} True if created, false if it already existed + */ + createConfig(template) { + this.createDirectory(); + let file = join(this.directory.path, 'config.json'); + if (!fs.existsSync(file)) { + this.client.log.plugins(`Creating plugin config file for "${this.name}"`); + fs.writeFileSync(file, JSON.stringify(template, null, 2)); + return true; + } else { + return false; + } + } + + /** + * The main function where your code should go. Create functions and event listeners here + */ + load() {} +}; \ No newline at end of file