import { logger } from '@squeebot/core/lib/core'; import { IRepository } from '@squeebot/core/lib/plugin/repository'; import { ReadLine } from 'readline'; import { Squeebot } from './core'; declare type Executable = (...args: string[]) => Promise; class CLICommand { constructor( public name: string, public func: Executable, public sub: CLICommand[], public after?: (returned: any, ...args: string[]) => Promise) {} public getSub(name: string): CLICommand | undefined { return this.sub.find(c => c.name.startsWith(name)); } /** * Executes a sub-command or, if not found, the current one. * @param args Command arguments */ public async execute(args: string[]): Promise { if (args[0]) { const subl = this.getSub(args[0]); if (subl) { const rv1 = await subl.execute(args.slice(1)); if (this.after) { return this.after.call(this, rv1, ...args); } return rv1; } } const rv2 = await this.func.call(this, ...args); if (this.after) { return this.after.call(this, rv2, ...args); } return rv2; } } function cmd(name: string, func: Executable, sub: CLICommand[] = []): CLICommand { return new CLICommand(name, func, sub); } export class SqueebotCLI { private inspector = false; private cmds: CLICommand[] = [ // Repository management cmd('repository', async (...args: string[]): Promise => { logger.log('Usage: repository add | update | remove | plugins | list'); }, [ cmd('install', async (...args: string[]): Promise => { if (!args.length) { throw new Error('URL is required'); } for (const urlp of args) { const repo = await this.bot.repositoryManager.installRepository(urlp); logger.log('Installed repository %s!', repo.name); } }), cmd('remove', async (...args: string[]): Promise => { if (!args.length) { throw new Error('Name is required'); } for (const namep of args) { await this.bot.repositoryManager.uninstallRepository(namep); logger.log('Installed repository %s.', namep); } }), cmd('update', async (...args: string[]): Promise => { if (!args.length) { const repolist = this.bot.repositoryManager.getAll(); for (const repo of repolist) { await this.checkUpdate(repo); } return; } for (const namep of args) { const repo = this.bot.repositoryManager.getRepoByName(namep); if (!repo) { throw new Error(`No such repository "${namep}" found.`); } await this.checkUpdate(repo); } }), cmd('plugins', async (...args: string[]): Promise => { if (!args.length) { throw new Error('Name is required'); } const repo = this.bot.repositoryManager.getRepoByName(args[0]); if (!repo) { throw new Error(`No such repository "${args[0]}" found.`); } logger.log('List of plugins in %s:', args[0], repo.plugins.map(x => `${x.name}@${x.version}`).join(', ')); }), cmd('list', async (...args: string[]): Promise => { const repos = this.bot.repositoryManager.getAll(); logger.log('List of installed repositories:'); for (const repo of repos) { logger.log('%s: (%s) with %d plugins | Date: %s', repo.name, repo.url, repo.plugins.length, new Date(repo.created * 1000).toDateString()); } }), ]), // Plugin management cmd('plugin', async (...args: string[]): Promise => { logger.log('Usage: plugin install | remove | enable | disable | load | restart | kill | list | running []'); }, [ cmd('install', async (...args: string[]): Promise => { for (const name of args) { const mf = await this.bot.repositoryManager.installPlugin(name); logger.log('Installed plugin %s version %s!', mf.name, mf.version); } }), cmd('restart', async (...args: string[]): Promise => { for (const name of args) { const plugin = this.bot.pluginManager.getAvailableByName(name); if (!plugin) { throw new Error(`"${name}" is not available. Maybe try installing it? plugin install ${name}`); } logger.log('Scheduling restart for', name); await this.bot.pluginManager.restart(plugin); } }), cmd('remove', async (...args: string[]): Promise => { for (const name of args) { await this.bot.repositoryManager.uninstallPlugin(name); logger.log('Uninstalled plugin %s.', name); } }), cmd('running', async (...args: string[]): Promise => { logger.log('Currently running plugins:', this.bot.pluginManager.getLoaded().map((p) => p.manifest.name).join(', ')); }), cmd('load', async (...args: string[]): Promise => { for (const name of args) { const plugin = this.bot.pluginManager.getAvailableByName(name); if (!plugin) { throw new Error(`"${name}" is not available. Maybe try installing it? plugin install`); } await this.bot.pluginManager.load(plugin); logger.log('Loaded plugin "%s" successfully.', name); } }), cmd('kill', async (...args: string[]): Promise => { for (const name of args) { if (!this.bot.pluginManager.getAvailableByName(name)) { throw new Error('No such plugin is available.'); } logger.log('Stopping plugin', name); this.bot.stream.emitTo(name, 'pluginUnload', name); } }), cmd('enable', async (...args: string[]): Promise => { for (const name of args) { if (!this.bot.pluginManager.getAvailableByName(name)) { throw new Error(`No such plugin "${name}" found.`); } logger.log('Enabling plugin', name); if (!this.bot.config.config.enabled) { this.bot.config.config.enabled = [name]; return; } if (this.bot.config.config.enabled.indexOf(name) === -1) { this.bot.config.config.enabled.push(name); } } await this.bot.config.save(); }), cmd('disable', async (...args: string[]): Promise => { for (const name of args) { if (!this.bot.config.config.enabled) { return; } logger.log('Disabling plugin', name); const indx = this.bot.config.config.enabled.indexOf(name); if (indx > -1) { this.bot.config.config.enabled.splice(indx, 1); } } await this.bot.config.save(); }), cmd('list', async (...args: string[]): Promise => { logger.log('Installed plugins:', this.bot.pluginManager.availablePlugins.map((mf) => mf.name).join(', ')); }), ]), // Channel management, with an "after" handler new CLICommand('channel', async (...args: string[]): Promise => { logger.log('Usage: channel new | del | list | addplugin | delplugin [] []'); }, [ cmd('new', async (...args: string[]): Promise => { if (this.bot.channelManager.getChannelByName(args[0])) { throw new Error('A channel by that name already exists!'); } this.bot.channelManager.addChannel({ name: args[0], plugins: [], enabled: true, }); logger.log('Channel added!'); }), cmd('remove', async (...args: string[]): Promise => { for (const name of args) { const chan = this.bot.channelManager.getChannelByName(name); if (!chan) { throw new Error(`No such channel "${name}" found.`); } this.bot.channelManager.removeChannel(chan); logger.log('Channel "%s" removed!', name); } }), cmd('addplugin', async (...args: string[]): Promise => { const chan1 = this.bot.channelManager.getChannelByName(args[0]); if (!chan1) { throw new Error('No such channel exists!'); } if (!args[1]) { throw new Error('A plugin name is required.'); } for (const name of args.slice(1)) { if (chan1.plugins.indexOf(name) === -1) { chan1.plugins.push(name); } logger.log('Plugin "%s" added to channel!', name); } }), cmd('delplugin', async (...args: string[]): Promise => { const chan2 = this.bot.channelManager.getChannelByName(args[0]); if (!chan2) { throw new Error('No such channel exists!'); } if (!args[1]) { throw new Error('A plugin name is required.'); } for (const name of args.slice(1)) { const idx = chan2.plugins.indexOf(name); if (idx !== -1) { chan2.plugins.splice(idx, 1); } logger.log('Plugin "%s" removed from channel!', name); } }), cmd('enable', async (...args: string[]): Promise => { for (const name of args) { const chan = this.bot.channelManager.getChannelByName(name); if (!chan) { throw new Error(`No such channel "${name}" found.`); } chan.enabled = true; logger.log('Channel "%s" enabled!', name); } }), cmd('disable', async (...args: string[]): Promise => { for (const name of args) { const chan = this.bot.channelManager.getChannelByName(name); if (!chan) { throw new Error(`No such channel "${name}" found.`); } chan.enabled = false; logger.log('Channel "%s" disabled!', name); } }), cmd('list', async (...args: string[]): Promise => { logger.log('Channels:\n', this.bot.channelManager.getAll().map((chan) => { return ` => ${chan.name}: ${chan.plugins.join(', ')} (${chan.enabled ? 'enabled' : 'disabled'})`; }).join('\n')); }), ], async (r: any, ...args: string[]): Promise => { // Don't save when we just printed help or a list if (!args.length || 'list'.startsWith(args[0])) { return r; } // Save current channel configuration this.bot.config.config.channels = this.bot.channelManager.getAll(); await this.bot.config.save(); return r; }), // Enter inspector mode. Every line entered will be executed as JavaScript. cmd('inspector', async (...args: string[]): Promise => { this.inspector = true; logger.warn('You have entered the JavaScript Inspector!'); console.log('Squeebot is available via "sb" or "this.bot".'); console.log('Type "exit" or "quit" to leave the inspector mode.'); }), cmd('quit', async (...args: string[]): Promise => this.bot.shutdown()), ]; constructor(private bot: Squeebot) {} private getCommand(name: string): CLICommand | undefined { return this.cmds.find(c => c.name.startsWith(name)); } private async checkUpdate(repo: IRepository): Promise { const updatable = await this.bot.repositoryManager.checkForUpdates(repo); if (updatable.length) { logger.log('[%s] The following plugins can be updated:', repo.name, updatable.map((u) => u.name).join(', ')); } else { logger.log('[%s] All plugins are up-to-date!', repo.name); } } private async sequentialExecute(cmds: string[][]): Promise { for (const argv of cmds) { if (!argv[0]) { logger.log('Available CLI Commands:', this.cmds .map(c => `(${c.name.charAt(0)})${c.name.substring(1)}`) .join(', ')); continue; } const clicmd = this.getCommand(argv[0]); if (!clicmd) { continue; } await clicmd.execute(argv.slice(1)); } } public attach(rl: ReadLine): void { rl.on('line', (line: string) => { // Inspector mode if (this.inspector) { if (line.startsWith('exit') || line.startsWith('quit')) { this.inspector = false; logger.warn('You have exited the JavaScript Inspector!'); return; } const sb = this.bot; try { // tslint:disable-next-line: no-eval console.log(eval(line)); } catch (e: any) { console.error(e.stack); } return; } // Executing commands in order. If previous command fails, the rest won't execute. const executeList = line.split('&&'); const toRun = executeList.map(subline => { return subline .replace(/\s+/g, ' ') .replace(/^\s+|\s+$/, '') .split(' ') .map(l => l.replace(',', '')); }); this.sequentialExecute(toRun).catch(e => logger.error(e.message)); }); } }