UNPKG

haxroomie-core

Version:

Node.js API for running and controlling HaxBall rooms.

398 lines (362 loc) 12 kB
/* global haxroomie */ /** * Object containing HHM plugin name and content. * * @typedef {Object} Plugin * @property {string} [name] - Plugins name. Can be overriden by the plugin * itself if it defines the `pluginSpec.name` property. * @property {string} content - UTF-8 encoded content of the plugin. */ /** * Object containing information about a plugin. * * @typedef {Object} PluginData * @property {number} id - The plugin id. * @property {string|number} name - The plugin name. * @property {boolean} isEnabled - Indicates whether the plugin is enabled or disabled. * @property {object} pluginSpec - HHM pluginSpec property. * @property {object} pluginSpecOriginal - Plugins initial pluginSpec (default values). */ /** * Class for controlling Haxball Headless Manager (HHM) plugins. */ class PluginController { constructor(opt) { this.page = opt.page; } /** * Returns loaded plugins. * * @returns {Promise<Array.<PluginData>>} - Array of plugins. */ async getPlugins() { let result = await this.page.evaluate(() => { let plugins = HHM.manager .getLoadedPluginIds() .map((id) => haxroomie.serializePlugin(id)) .filter((pluginData) => { const name = pluginData.pluginSpec.name; // ignore these plugins if (!haxroomie.ignoredPlugins) return true; return !haxroomie.ignoredPlugins.has(name); }); return plugins; }); return result; } /** * Returns PluginData of the given plugin name. * * @param {string} name - Name of the plugin. * @returns {Promise.<?PluginData>} - Data of the plugin or `null` if * plugin was not found. */ async getPlugin(name) { return this.page.evaluate((name) => { const plugin = HHM.manager.getPlugin(name); return haxroomie.serializePlugin(plugin); }, name); } /** * Enables a HHM plugin with the given name. * * @param {string} name - Name of the plugin * @returns {Promise.<boolean>} - `true` if plugin was enabled, `false` otherwise. */ async enablePlugin(name) { return this.page.evaluate((name) => { return HHM.manager.enablePlugin(name); }, name); } /** * Disables a HHM plugin with the given name. * * If the name is an Array then * it disables all the plugins in the given order. * * @param {string} name - Name the plugin. * @param {boolean} [recursive=false] - If true all the plugins that depend on * the plugin will get disabled also. * @returns {Promise.<Array.<number>>} - Array of disabled plugin IDs or * empty array if the plugin could not be disabled or was already disabled. */ async disablePlugin(name, recursive = false) { return this.page.evaluate( async (name, recursive) => { return HHM.manager.disablePlugin(name, recursive); }, name, recursive ); } /** * Gets a list of plugins that depend on the given plugin. * * @param {string} name - Name or id of the plugin. * @param {boolean} [recursive=true] - Finds indirect dependencies also. * @param {boolean} [includeDisabled=false] - Include disabled plugins * @returns {Promise<Array.<PluginData>>} - Array of plugins. */ async getPluginsThatDependOn( name, recursive = true, includeDisabled = false ) { return this.page.evaluate( (name, recursive, includeDisabled) => { return HHM.manager .getDependentPlugins(name, recursive, includeDisabled) .map((id) => haxroomie.serializePlugin(id)); }, name, recursive, includeDisabled ); } /** * Checks if the room has a plugin with given name loaded. * @param {string} name - Name of the plugin. * @returns {Promise.<boolean>} - `true` if it had the plugin, `false` if not. */ async hasPlugin(name) { return this.page.evaluate(async (name) => { return HHM.manager.hasPlugin(name); }, name); } /** * Adds a new plugin. * * If the `plugin` is `string`, then it will be loaded from the available * repositories. * * If the `plugin` is [Plugin]{@link Plugin}, then it will be loaded * from contents of `Plugin`. * * @param {string|Plugin} plugin - Plugins name if loading from * repositories or plugin definition if loading it from an object. * @param {object} [pluginConfig] - Configuration options for the plugin. * @returns {Promise.<number>} - Plugin ID if the plugin and all of its dependencies * have been loaded, -1 otherwise. */ async addPlugin(plugin, pluginConfig) { if (!plugin) { throw new TypeError('Missing required argument: plugin'); } if (typeof plugin === 'string') { return this.page.evaluate( async (name, pluginConfig) => { let id = await HHM.manager.addPlugin({ pluginName: name }); if (id >= 0 && pluginConfig) { HHM.manager.setPluginConfig(id, pluginConfig); } return id; }, plugin, pluginConfig ); } if (!plugin.content) { throw new TypeError('Plugin is missing required property: content'); } return this.page.evaluate( async (plugin, pluginConfig) => { let id = await HHM.manager.addPlugin({ pluginCode: plugin.content, pluginName: plugin.name, }); if (id >= 0 && pluginConfig) { HHM.manager.setPluginConfig(id, pluginConfig); } return id; }, plugin, pluginConfig ); } /** * Removes a plugin. * * @param {string} pluginName - Plugins name. * @param {boolean} [safe=true] - Remove plugin safely (see HHM * PluginManager#removePlugin). * @returns {Promise.<boolean>} - Whether the removal was successful. */ async removePlugin(pluginName, safe = true) { if (!pluginName) { throw new TypeError('Missing required argument: pluginName'); } return this.page.evaluate( async (pluginName, safe) => { let id = HHM.manager.getPluginId(pluginName); return HHM.manager.removePlugin(id, safe); }, pluginName, safe ); } /** * Sets the rooms plugin config. Merges the new config with the old one. * * Tries to load plugins that are not loaded from the available * repositories and removes the loaded plugins that are not in the given * config. * * If `pluginName` is given then only config for the given plugin * is set. * @param {object} pluginConfig - Room wide config or plugins config. * @param {string} [pluginName] - Name of the plugin if wanting to change * config of only one plugin. */ async setPluginConfig(pluginConfig, pluginName) { if (!pluginConfig) { throw new TypeError('Missing required argument: pluginConfig'); } if (typeof pluginConfig !== 'object') { throw new TypeError('typeof pluginConfig should be object'); } // Set the config for one plugin if plugins name is given. if (typeof pluginName === 'string') { await this.page.evaluate( async (pluginName, pluginConfig) => { let pluginId = HHM.manager.getPluginId(pluginName); if (pluginId < 0) { pluginId = await HHM.manager.addPlugin({ pluginName }); if (pluginId < 0) { throw new Error( `Cannot load plugin "${pluginName}" from available repositories.` ); } } let plugin = HHM.manager.getPlugin(pluginName); // get plugins default config const pluginDefaultConfig = plugin._pluginSpecOriginal.config ? plugin._pluginSpecOriginal.config : {}; // merge the new config with plugins default values HHM.manager.setPluginConfig(pluginId, { ...pluginDefaultConfig, ...pluginConfig, }); }, pluginName, pluginConfig ); return; } // Change the configs for all the plugins if no plugin name is given. for (let [name, config] of Object.entries(pluginConfig)) { await this.page.evaluate( async (name, config) => { let pluginId = HHM.manager.getPluginId(name); if (pluginId < 0) { pluginId = await HHM.manager.addPlugin({ pluginName: name }); if (pluginId < 0) { return; } } let plugin = HHM.manager.getPlugin(name); // get plugins default config const pluginDefaultConfig = plugin._pluginSpecOriginal.config ? plugin._pluginSpecOriginal.config : {}; // merge the new config with plugins default values HHM.manager.setPluginConfig(pluginId, { ...pluginDefaultConfig, ...config, }); }, name, config ); } await this.prunePlugins({ pluginConfig }); } /** * Removes the plugins that are not in the given `pluginConfig`. * * Goes over the loaded plugins that are not in the pluginConfig and * checks if they are a direct or indirect dependency of one of the plugins * listed in the pluginConfig and remove them if not. * @private */ async prunePlugins({ pluginConfig }) { const loadedPlugins = await this.getPlugins(); for (let plugin of loadedPlugins) { if (Object.prototype.hasOwnProperty.call(pluginConfig, plugin.name)) { continue; } let dependentPlugins = await this.getPluginsThatDependOn( plugin.name, true, true ); let shouldBeRemoved = true; for (let dependent of dependentPlugins) { if (!dependent) continue; if ( Object.prototype.hasOwnProperty.call(pluginConfig, dependent.name) ) { shouldBeRemoved = false; break; } } if (shouldBeRemoved) { await this.disablePlugin(plugin.name, true); await this.removePlugin(plugin.name, false); } } } /** * Returns the plugin config for all loaded plugins in the room or * if `pluginName` is given, then return the config for that plugin. * * @param {string} [pluginName] - The name of the plugin. * @returns {Promise.<object>} - The config object of plugin(s). */ async getPluginConfig(pluginName) { if (typeof pluginName === 'string') { let config = await this.page.evaluate((pluginName) => { let plugin = HHM.manager.getPlugin(pluginName); if (!plugin) { throw new TypeError(`Invalid plugin "${pluginName}".`); } return plugin.getConfig(); }, pluginName); return config; } let config = await this.page.evaluate(() => { let plugins = HHM.manager.getLoadedPluginIds().map((id) => { return HHM.manager.getPlugin(id); }); let cfg = {}; for (let plugin of plugins) { cfg[plugin] = plugin.getConfig(); } return cfg; }); return config; } /** * Reloads a plugin from the configured repositories. * * This is a wrapper around the HHM PluginManager.reloadPlugin method. * See https://github.com/saviola777/haxball-headless-manager/blob/master/src/classes/PluginManager.js#L565 * * @param {string} pluginName Plugin name to be reloaded. * @param {boolean} [safe] Whether to disable dependent plugins before * unloading the given plugin. * @returns {boolean} Whether the plugin was successfully reloaded * @throws {Error} If the given plugin is not loaded or if safe mode was * enabled but the plugin can't be disabled. */ async reloadPlugin(pluginName, safe = true) { return this.page.evaluate( (pluginName, safe) => { return HHM.manager.reloadPlugin(pluginName, safe); }, pluginName, safe ); } } module.exports = PluginController;