plugin-registerable control commands

This commit is contained in:
Evert Prants 2021-01-31 15:22:49 +02:00
parent 51561a1d42
commit 293f949afc
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
3 changed files with 363 additions and 238 deletions

View File

@ -3,7 +3,7 @@
"name": "control", "name": "control",
"description": "Squeebot Plugin Management API and sockets", "description": "Squeebot Plugin Management API and sockets",
"tags": ["api", "control", "management"], "tags": ["api", "control", "management"],
"version": "0.0.0", "version": "0.1.0",
"dependencies": [], "dependencies": [],
"npmDependencies": [] "npmDependencies": []
} }

View File

@ -86,248 +86,354 @@ const sc = {
}; };
*/ */
const ControlCommands: { [key: string]: Function } = { interface ControlCommand {
loadPlugin: async (p: ControlPlugin, plugin: string): Promise<void> => { execute: Function;
if (!plugin) { name: string;
throw new Error('This function takes 1 argument.'); plugin: string;
} }
p.stream.emitTo('core', 'pluginLoad', plugin);
},
unloadPlugin: async (p: ControlPlugin, plugin: string): Promise<void> => {
if (!plugin) {
throw new Error('This function takes 1 argument.');
}
p.stream.emitTo(plugin, 'pluginUnload', plugin);
},
listActivePlugins: async (p: ControlPlugin): Promise<IPluginManifest[]> => {
return p.core!.pluginManager.getLoaded().map((x: IPlugin) => x.manifest);
},
listInstalledPlugins: async (p: ControlPlugin): Promise<IPluginManifest[]> => {
return p.core!.pluginManager.availablePlugins;
},
installPlugin: async (p: ControlPlugin, plugin: string): Promise<IPluginManifest> => {
if (!plugin) {
throw new Error('This function takes 1 argument.');
}
return p.core!.repositoryManager.installPlugin(plugin);
},
uninstallPlugin: async (p: ControlPlugin, plugin: string): Promise<void> => {
if (!plugin) {
throw new Error('This function takes 1 argument.');
}
return p.core!.repositoryManager.uninstallPlugin(plugin);
},
enablePlugin: async (p: ControlPlugin, plugin: string): Promise<void> => {
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) { let controlCommands: ControlCommand[] = [
p.core!.config.config.enabled = [plugin]; {
} else if (p.core!.config.config.enabled.indexOf(plugin) === -1) { name: 'execute',
p.core!.config.config.enabled.push(plugin); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<void> => {
if (!plugin) {
return p.core!.config.save(); throw new Error('This function takes 1 argument.');
},
disablePlugin: async (p: ControlPlugin, plugin: string): Promise<void> => {
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<IRepository> => {
if (!url) {
throw new Error('This function takes 1 argument.');
}
return p.core!.repositoryManager.installRepository(url);
},
uninstallRepository: async (p: ControlPlugin, repo: string): Promise<void> => {
if (!repo) {
throw new Error('This function takes 1 argument.');
}
return p.core!.repositoryManager.uninstallRepository(repo);
},
listRepositoryPlugins: async (p: ControlPlugin, repo: string): Promise<IRepoPluginDef[]> => {
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<IRepository[]> => {
return p.core!.repositoryManager.getAll();
},
updateRepository: async (p: ControlPlugin, repo: string): Promise<IPluginManifest[]> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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);
} }
} p.stream.emitTo('core', 'pluginLoad', plugin);
},
}, },
removeChannelPlugin: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise<void> => { {
if (!name || !plugins) { name: 'unloadPlugin',
throw new Error('This function takes 2 arguments.'); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<void> => {
const chan = p.core!.channelManager.getChannelByName(name); if (!plugin) {
if (!chan) { throw new Error('This function takes 1 argument.');
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);
} }
} p.stream.emitTo(plugin, 'pluginUnload', plugin);
},
}, },
listChannels: async (p: ControlPlugin): Promise<IChannel[]> => { {
return p.core!.channelManager.getAll(); name: 'listActivePlugins',
plugin: 'control',
execute: async (p: ControlPlugin): Promise<IPluginManifest[]> => {
return p.core!.pluginManager.getLoaded().map((x: IPlugin) => x.manifest);
},
}, },
getPluginConfig: async (p: ControlPlugin, name: string): Promise<any> => { {
if (!name) { name: 'listInstalledPlugins',
throw new Error('This function takes 1 argument.'); plugin: 'control',
} execute: async (p: ControlPlugin): Promise<IPluginManifest[]> => {
const plugin = p.core!.pluginManager.getLoadedByName(name); return p.core!.pluginManager.availablePlugins;
if (!plugin) { },
throw new Error('No such plugin is currently running!');
}
return plugin.config.config;
}, },
getPluginConfigValue: async (p: ControlPlugin, name: string, key: string): Promise<any> => { {
if (!name || !key) { name: 'installPlugin',
throw new Error('This function takes 2 arguments.'); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<IPluginManifest> => {
const plugin = p.core!.pluginManager.getLoadedByName(name); if (!plugin) {
if (!plugin) { throw new Error('This function takes 1 argument.');
throw new Error('No such plugin is currently running!'); }
} return p.core!.repositoryManager.installPlugin(plugin);
return plugin.config.get(key); },
}, },
getPluginConfigSchema: async (p: ControlPlugin, name: string): Promise<any> => { {
if (!name) { name: 'uninstallPlugin',
throw new Error('This function takes 1 argument.'); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<void> => {
const plugin = p.plugins.get(name); if (!plugin) {
if (!plugin) { throw new Error('This function takes 1 argument.');
throw new Error('This plugin has not registered a schema in control.'); }
} return p.core!.repositoryManager.uninstallPlugin(plugin);
return plugin; },
}, },
setPluginConfig: async (p: ControlPlugin, name: string, config: any): Promise<any> => { {
if (!name || !config) { name: 'enablePlugin',
throw new Error('This function takes 2 arguments.'); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<void> => {
const plugin = p.core!.pluginManager.getLoadedByName(name); if (!plugin) {
if (!plugin) { throw new Error('This function takes 1 argument.');
throw new Error('No such plugin is currently running!'); }
} if (!p.core!.pluginManager.getAvailableByName(plugin)) {
plugin.config.config = config; throw new Error('No such plugin is available.');
return plugin.config.save(); }
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<any> => { {
if (!name || !key) { name: 'disablePlugin',
throw new Error('This function takes 3 arguments.'); plugin: 'control',
} execute: async (p: ControlPlugin, plugin: string): Promise<void> => {
const plugin = p.core!.pluginManager.getLoadedByName(name); if (!plugin) {
if (!plugin) { throw new Error('This function takes 1 argument.');
throw new Error('No such plugin is currently running!'); }
} if (!p.core!.pluginManager.getAvailableByName(plugin)) {
plugin.config.set(key, value); throw new Error('No such plugin is available.');
return plugin.config.save(); }
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<IRepository> => {
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<void> => {
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<IRepoPluginDef[]> => {
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<IRepository[]> => {
return p.core!.repositoryManager.getAll();
},
},
{
name: 'updateRepository',
plugin: 'control',
execute: async (p: ControlPlugin, repo: string): Promise<IPluginManifest[]> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<IChannel[]> => {
return p.core!.channelManager.getAll();
},
},
{
name: 'getPluginConfig',
plugin: 'control',
execute: async (p: ControlPlugin, name: string): Promise<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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}; declare type StringAny = {[key: string]: any};
const match = ['key', 'cert', 'ca', 'dhparam', 'crl', 'pfx']; const match = ['key', 'cert', 'ca', 'dhparam', 'crl', 'pfx'];
@ -448,7 +554,7 @@ class ControlPlugin extends Plugin {
socket.setEncoding('utf8'); socket.setEncoding('utf8');
socket.write(JSON.stringify({ socket.write(JSON.stringify({
status: 'OK', status: 'OK',
commands: Object.keys(ControlCommands), commands: controlCommands.map(k => k.name),
}) + '\r\n'); }) + '\r\n');
socket.on('data', (data) => { socket.on('data', (data) => {
@ -488,6 +594,8 @@ class ControlPlugin extends Plugin {
parseTLSConfig(c.tls).then((options) => { parseTLSConfig(c.tls).then((options) => {
this.server = tls.createServer(options, this.server = tls.createServer(options,
(socket) => this.handleIncoming(socket)); (socket) => this.handleIncoming(socket));
this.server.on('error',
(e) => logger.error('[%s] Secure socket error:', e.message));
this.server.listen(c.bind, () => { this.server.listen(c.bind, () => {
logger.log('[%s] Secure socket listening on %s', logger.log('[%s] Secure socket listening on %s',
this.name, c.bind.toString()); this.name, c.bind.toString());
@ -508,6 +616,7 @@ class ControlPlugin extends Plugin {
} }
this.server = net.createServer((socket) => this.handleIncoming(socket)); 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, () => { this.server.listen(c.bind, () => {
logger.log('[%s] Socket listening on %s', logger.log('[%s] Socket listening on %s',
this.name, c.bind.toString()); this.name, c.bind.toString());
@ -522,10 +631,23 @@ class ControlPlugin extends Plugin {
if (!this.core) { if (!this.core) {
throw new Error('The control plugin cannot control the bot right now.'); 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'); 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') @EventListener('pluginUnload')
@ -550,6 +672,9 @@ class ControlPlugin extends Plugin {
plugin = plugin.manifest.name; plugin = plugin.manifest.name;
} }
this.plugins.delete(plugin); this.plugins.delete(plugin);
controlCommands = controlCommands.filter(k => {
return k.plugin !== plugin;
});
} }
} }
} }

View File

@ -3,15 +3,15 @@
"plugins": [ "plugins": [
{ {
"name": "control", "name": "control",
"version": "0.0.0" "version": "0.1.0"
}, },
{ {
"name": "permissions", "name": "permissions",
"version": "0.0.0" "version": "0.1.0"
}, },
{ {
"name": "simplecommands", "name": "simplecommands",
"version": "1.1.0" "version": "1.1.1"
} }
], ],
"typescript": true "typescript": true