From 293f949afc009983a19cb4a88fffe6c24db088a9 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 31 Jan 2021 15:22:49 +0200 Subject: [PATCH] plugin-registerable control commands --- control/plugin.json | 2 +- control/plugin.ts | 593 +++++++++++++++++++++++++++----------------- squeebot.repo.json | 6 +- 3 files changed, 363 insertions(+), 238 deletions(-) diff --git a/control/plugin.json b/control/plugin.json index ba48811..ef5c95e 100644 --- a/control/plugin.json +++ b/control/plugin.json @@ -3,7 +3,7 @@ "name": "control", "description": "Squeebot Plugin Management API and sockets", "tags": ["api", "control", "management"], - "version": "0.0.0", + "version": "0.1.0", "dependencies": [], "npmDependencies": [] } diff --git a/control/plugin.ts b/control/plugin.ts index ba9a6fe..621838c 100644 --- a/control/plugin.ts +++ b/control/plugin.ts @@ -86,248 +86,354 @@ const sc = { }; */ -const ControlCommands: { [key: string]: Function } = { - loadPlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - p.stream.emitTo('core', 'pluginLoad', plugin); - }, - unloadPlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - p.stream.emitTo(plugin, 'pluginUnload', plugin); - }, - listActivePlugins: async (p: ControlPlugin): Promise => { - return p.core!.pluginManager.getLoaded().map((x: IPlugin) => x.manifest); - }, - listInstalledPlugins: async (p: ControlPlugin): Promise => { - return p.core!.pluginManager.availablePlugins; - }, - installPlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - return p.core!.repositoryManager.installPlugin(plugin); - }, - uninstallPlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - return p.core!.repositoryManager.uninstallPlugin(plugin); - }, - enablePlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - if (!p.core!.pluginManager.getAvailableByName(plugin)) { - throw new Error('No such plugin is available.'); - } +interface ControlCommand { + execute: Function; + name: string; + plugin: string; +} - if (!p.core!.config.config.enabled) { - p.core!.config.config.enabled = [plugin]; - } else if (p.core!.config.config.enabled.indexOf(plugin) === -1) { - p.core!.config.config.enabled.push(plugin); - } - - return p.core!.config.save(); - }, - disablePlugin: async (p: ControlPlugin, plugin: string): Promise => { - if (!plugin) { - throw new Error('This function takes 1 argument.'); - } - if (!p.core!.pluginManager.getAvailableByName(plugin)) { - throw new Error('No such plugin is available.'); - } - if (!p.core!.config.config.enabled) { - return; - } - const indx = p.core!.config.config.enabled.indexOf(plugin); - - if (indx > -1) { - p.core!.config.config.enabled.splice(indx, 1); - } - - return p.core!.config.save(); - }, - installRepository: async (p: ControlPlugin, url: string): Promise => { - if (!url) { - throw new Error('This function takes 1 argument.'); - } - return p.core!.repositoryManager.installRepository(url); - }, - uninstallRepository: async (p: ControlPlugin, repo: string): Promise => { - if (!repo) { - throw new Error('This function takes 1 argument.'); - } - return p.core!.repositoryManager.uninstallRepository(repo); - }, - listRepositoryPlugins: async (p: ControlPlugin, repo: string): Promise => { - if (!repo) { - throw new Error('This function takes 1 argument.'); - } - const repoData = p.core!.repositoryManager.getRepoByName(repo); - if (!repoData) { - throw new Error('No such repository found.'); - } - return repoData.plugins; - }, - listRepositories: async (p: ControlPlugin): Promise => { - return p.core!.repositoryManager.getAll(); - }, - updateRepository: async (p: ControlPlugin, repo: string): Promise => { - if (!repo) { - throw new Error('This function takes 1 argument.'); - } - const repoData = p.core!.repositoryManager.getRepoByName(repo); - if (!repoData) { - throw new Error('No such repository found.'); - } - return p.core!.repositoryManager.checkForUpdates(repoData); - }, - newChannel: async (p: ControlPlugin, name: string, plugins?: string[]): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - if (p.core!.channelManager.getChannelByName(name)) { - throw new Error('A channel by that name already exists!'); - } - - p.core!.channelManager.addChannel({ - name, - plugins: plugins || [], - enabled: true, - }); - }, - removeChannel: async (p: ControlPlugin, name: string): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - const chan = p.core!.channelManager.getChannelByName(name); - if (!chan) { - throw new Error('A channel by that name does not exists!'); - } - - p.core!.channelManager.removeChannel(chan); - }, - enableChannel: async (p: ControlPlugin, name: string): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - const chan = p.core!.channelManager.getChannelByName(name); - if (!chan) { - throw new Error('A channel by that name does not exists!'); - } - - chan.enabled = true; - p.core!.config.config.channels = p.core!.channelManager.getAll(); - await p.core!.config.save(); - }, - disableChannel: async (p: ControlPlugin, name: string): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - const chan = p.core!.channelManager.getChannelByName(name); - if (!chan) { - throw new Error('A channel by that name does not exists!'); - } - - chan.enabled = false; - p.core!.config.config.channels = p.core!.channelManager.getAll(); - await p.core!.config.save(); - }, - addChannelPlugin: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise => { - if (!name || !plugins) { - throw new Error('This function takes 2 arguments.'); - } - const chan = p.core!.channelManager.getChannelByName(name); - if (!chan) { - throw new Error('A channel by that name does not exists!'); - } - if (!Array.isArray(plugins)) { - plugins = [plugins]; - } - for (const plugin of plugins) { - if (chan.plugins.indexOf(plugin) === -1) { - chan.plugins.push(plugin); +let controlCommands: ControlCommand[] = [ + { + name: 'execute', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); } - } + p.stream.emitTo('core', 'pluginLoad', plugin); + }, }, - removeChannelPlugin: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise => { - if (!name || !plugins) { - throw new Error('This function takes 2 arguments.'); - } - const chan = p.core!.channelManager.getChannelByName(name); - if (!chan) { - throw new Error('A channel by that name does not exists!'); - } - if (!Array.isArray(plugins)) { - plugins = [plugins]; - } - for (const plugin of plugins) { - const idx = chan.plugins.indexOf(plugin); - if (idx !== -1) { - chan.plugins.splice(idx, 1); + { + name: 'unloadPlugin', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); } - } + p.stream.emitTo(plugin, 'pluginUnload', plugin); + }, }, - listChannels: async (p: ControlPlugin): Promise => { - return p.core!.channelManager.getAll(); + { + name: 'listActivePlugins', + plugin: 'control', + execute: async (p: ControlPlugin): Promise => { + return p.core!.pluginManager.getLoaded().map((x: IPlugin) => x.manifest); + }, }, - getPluginConfig: async (p: ControlPlugin, name: string): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - const plugin = p.core!.pluginManager.getLoadedByName(name); - if (!plugin) { - throw new Error('No such plugin is currently running!'); - } - return plugin.config.config; + { + name: 'listInstalledPlugins', + plugin: 'control', + execute: async (p: ControlPlugin): Promise => { + return p.core!.pluginManager.availablePlugins; + }, }, - getPluginConfigValue: async (p: ControlPlugin, name: string, key: string): Promise => { - if (!name || !key) { - throw new Error('This function takes 2 arguments.'); - } - const plugin = p.core!.pluginManager.getLoadedByName(name); - if (!plugin) { - throw new Error('No such plugin is currently running!'); - } - return plugin.config.get(key); + { + name: 'installPlugin', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); + } + return p.core!.repositoryManager.installPlugin(plugin); + }, }, - getPluginConfigSchema: async (p: ControlPlugin, name: string): Promise => { - if (!name) { - throw new Error('This function takes 1 argument.'); - } - const plugin = p.plugins.get(name); - if (!plugin) { - throw new Error('This plugin has not registered a schema in control.'); - } - return plugin; + { + name: 'uninstallPlugin', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); + } + return p.core!.repositoryManager.uninstallPlugin(plugin); + }, }, - setPluginConfig: async (p: ControlPlugin, name: string, config: any): Promise => { - if (!name || !config) { - throw new Error('This function takes 2 arguments.'); - } - const plugin = p.core!.pluginManager.getLoadedByName(name); - if (!plugin) { - throw new Error('No such plugin is currently running!'); - } - plugin.config.config = config; - return plugin.config.save(); + { + name: 'enablePlugin', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); + } + if (!p.core!.pluginManager.getAvailableByName(plugin)) { + throw new Error('No such plugin is available.'); + } + + if (!p.core!.config.config.enabled) { + p.core!.config.config.enabled = [plugin]; + } else if (p.core!.config.config.enabled.indexOf(plugin) === -1) { + p.core!.config.config.enabled.push(plugin); + } + + return p.core!.config.save(); + }, }, - setPluginConfigValue: async (p: ControlPlugin, name: string, key: string, value: string): Promise => { - if (!name || !key) { - throw new Error('This function takes 3 arguments.'); - } - const plugin = p.core!.pluginManager.getLoadedByName(name); - if (!plugin) { - throw new Error('No such plugin is currently running!'); - } - plugin.config.set(key, value); - return plugin.config.save(); + { + name: 'disablePlugin', + plugin: 'control', + execute: async (p: ControlPlugin, plugin: string): Promise => { + if (!plugin) { + throw new Error('This function takes 1 argument.'); + } + if (!p.core!.pluginManager.getAvailableByName(plugin)) { + throw new Error('No such plugin is available.'); + } + if (!p.core!.config.config.enabled) { + return; + } + const indx = p.core!.config.config.enabled.indexOf(plugin); + + if (indx > -1) { + p.core!.config.config.enabled.splice(indx, 1); + } + + return p.core!.config.save(); + }, }, -}; + { + name: 'installRepository', + plugin: 'control', + execute: async (p: ControlPlugin, url: string): Promise => { + if (!url) { + throw new Error('This function takes 1 argument.'); + } + return p.core!.repositoryManager.installRepository(url); + }, + }, + { + name: 'uninstallRepository', + plugin: 'control', + execute: async (p: ControlPlugin, repo: string): Promise => { + if (!repo) { + throw new Error('This function takes 1 argument.'); + } + return p.core!.repositoryManager.uninstallRepository(repo); + }, + }, + { + name: 'listRepositoryPlugins', + plugin: 'control', + execute: async (p: ControlPlugin, repo: string): Promise => { + if (!repo) { + throw new Error('This function takes 1 argument.'); + } + const repoData = p.core!.repositoryManager.getRepoByName(repo); + if (!repoData) { + throw new Error('No such repository found.'); + } + return repoData.plugins; + }, + }, + { + name: 'listRepositories', + plugin: 'control', + execute: async (p: ControlPlugin): Promise => { + return p.core!.repositoryManager.getAll(); + }, + }, + { + name: 'updateRepository', + plugin: 'control', + execute: async (p: ControlPlugin, repo: string): Promise => { + if (!repo) { + throw new Error('This function takes 1 argument.'); + } + const repoData = p.core!.repositoryManager.getRepoByName(repo); + if (!repoData) { + throw new Error('No such repository found.'); + } + return p.core!.repositoryManager.checkForUpdates(repoData); + }, + }, + { + name: 'newChannel', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, plugins?: string[]): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + if (p.core!.channelManager.getChannelByName(name)) { + throw new Error('A channel by that name already exists!'); + } + + p.core!.channelManager.addChannel({ + name, + plugins: plugins || [], + enabled: true, + }); + }, + }, + { + name: 'removeChannel', + plugin: 'control', + execute: async (p: ControlPlugin, name: string): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + const chan = p.core!.channelManager.getChannelByName(name); + if (!chan) { + throw new Error('A channel by that name does not exists!'); + } + + p.core!.channelManager.removeChannel(chan); + }, + }, + { + name: 'enableChannel', + plugin: 'control', + execute: async (p: ControlPlugin, name: string): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + const chan = p.core!.channelManager.getChannelByName(name); + if (!chan) { + throw new Error('A channel by that name does not exists!'); + } + + chan.enabled = true; + p.core!.config.config.channels = p.core!.channelManager.getAll(); + await p.core!.config.save(); + }, + }, + { + name: 'disableChannel', + plugin: 'control', + execute: async (p: ControlPlugin, name: string): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + const chan = p.core!.channelManager.getChannelByName(name); + if (!chan) { + throw new Error('A channel by that name does not exists!'); + } + + chan.enabled = false; + p.core!.config.config.channels = p.core!.channelManager.getAll(); + await p.core!.config.save(); + }, + }, + { + name: 'addChannelPlugin', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise => { + if (!name || !plugins) { + throw new Error('This function takes 2 arguments.'); + } + const chan = p.core!.channelManager.getChannelByName(name); + if (!chan) { + throw new Error('A channel by that name does not exists!'); + } + if (!Array.isArray(plugins)) { + plugins = [plugins]; + } + for (const plugin of plugins) { + if (chan.plugins.indexOf(plugin) === -1) { + chan.plugins.push(plugin); + } + } + }, + }, + { + name: 'removeChannelPlugin', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise => { + if (!name || !plugins) { + throw new Error('This function takes 2 arguments.'); + } + const chan = p.core!.channelManager.getChannelByName(name); + if (!chan) { + throw new Error('A channel by that name does not exists!'); + } + if (!Array.isArray(plugins)) { + plugins = [plugins]; + } + for (const plugin of plugins) { + const idx = chan.plugins.indexOf(plugin); + if (idx !== -1) { + chan.plugins.splice(idx, 1); + } + } + }, + }, + { + name: 'listChannels', + plugin: 'control', + execute: async (p: ControlPlugin): Promise => { + return p.core!.channelManager.getAll(); + }, + }, + { + name: 'getPluginConfig', + plugin: 'control', + execute: async (p: ControlPlugin, name: string): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + const plugin = p.core!.pluginManager.getLoadedByName(name); + if (!plugin) { + throw new Error('No such plugin is currently running!'); + } + return plugin.config.config; + }, + }, + { + name: 'getPluginConfigValue', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, key: string): Promise => { + if (!name || !key) { + throw new Error('This function takes 2 arguments.'); + } + const plugin = p.core!.pluginManager.getLoadedByName(name); + if (!plugin) { + throw new Error('No such plugin is currently running!'); + } + return plugin.config.get(key); + }, + }, + { + name: 'getPluginConfigSchema', + plugin: 'control', + execute: async (p: ControlPlugin, name: string): Promise => { + if (!name) { + throw new Error('This function takes 1 argument.'); + } + const plugin = p.plugins.get(name); + if (!plugin) { + throw new Error('This plugin has not registered a schema in control.'); + } + return plugin; + }, + }, + { + name: 'setPluginConfig', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, config: any): Promise => { + if (!name || !config) { + throw new Error('This function takes 2 arguments.'); + } + const plugin = p.core!.pluginManager.getLoadedByName(name); + if (!plugin) { + throw new Error('No such plugin is currently running!'); + } + plugin.config.config = config; + return plugin.config.save(); + }, + }, + { + name: 'setPluginConfigValue', + plugin: 'control', + execute: async (p: ControlPlugin, name: string, key: string, value: string): Promise => { + if (!name || !key) { + throw new Error('This function takes 3 arguments.'); + } + const plugin = p.core!.pluginManager.getLoadedByName(name); + if (!plugin) { + throw new Error('No such plugin is currently running!'); + } + plugin.config.set(key, value); + return plugin.config.save(); + }, + }, +]; declare type StringAny = {[key: string]: any}; const match = ['key', 'cert', 'ca', 'dhparam', 'crl', 'pfx']; @@ -448,7 +554,7 @@ class ControlPlugin extends Plugin { socket.setEncoding('utf8'); socket.write(JSON.stringify({ status: 'OK', - commands: Object.keys(ControlCommands), + commands: controlCommands.map(k => k.name), }) + '\r\n'); socket.on('data', (data) => { @@ -488,6 +594,8 @@ class ControlPlugin extends Plugin { parseTLSConfig(c.tls).then((options) => { this.server = tls.createServer(options, (socket) => this.handleIncoming(socket)); + this.server.on('error', + (e) => logger.error('[%s] Secure socket error:', e.message)); this.server.listen(c.bind, () => { logger.log('[%s] Secure socket listening on %s', this.name, c.bind.toString()); @@ -508,6 +616,7 @@ class ControlPlugin extends Plugin { } this.server = net.createServer((socket) => this.handleIncoming(socket)); + this.server.on('error', (e) => logger.error('[%s] Socket error:', e.message)); this.server.listen(c.bind, () => { logger.log('[%s] Socket listening on %s', this.name, c.bind.toString()); @@ -522,10 +631,23 @@ class ControlPlugin extends Plugin { if (!this.core) { throw new Error('The control plugin cannot control the bot right now.'); } - if (!(command in ControlCommands)) { + const cmdobj = controlCommands.find(k => k.name === command); + if (!cmdobj || !cmdobj.execute) { throw new Error('No such command'); } - return ControlCommands[command].call(this, this, ...args); + return cmdobj.execute.call(this, this, ...args); + } + + public registerControlCommand(obj: ControlCommand): void { + if (!obj.execute || !obj.name || !obj.plugin) { + throw new Error('Invalid command object.'); + } + const exists = controlCommands.find(k => k.name === obj.name); + if (exists) { + throw new Error('Control commands should not be overwritten.'); + } + controlCommands.push(obj); + logger.log('[%s] registered control command', this.name, obj.name); } @EventListener('pluginUnload') @@ -550,6 +672,9 @@ class ControlPlugin extends Plugin { plugin = plugin.manifest.name; } this.plugins.delete(plugin); + controlCommands = controlCommands.filter(k => { + return k.plugin !== plugin; + }); } } } diff --git a/squeebot.repo.json b/squeebot.repo.json index e75f897..25bcc11 100644 --- a/squeebot.repo.json +++ b/squeebot.repo.json @@ -3,15 +3,15 @@ "plugins": [ { "name": "control", - "version": "0.0.0" + "version": "0.1.0" }, { "name": "permissions", - "version": "0.0.0" + "version": "0.1.0" }, { "name": "simplecommands", - "version": "1.1.0" + "version": "1.1.1" } ], "typescript": true