UNPKG

concierge-bot

Version:

Extensible general purpose chat bot.

228 lines (205 loc) 8.54 kB
/** * Main platform. Handles the core interop of the program and * acts as the glue code for the various parts of the code. * * Written By: * Matthew Knox * * License: * MIT License. All code unless otherwise specified is * Copyright (c) Matthew Knox and Contributors 2017. */ const figlet = require('figlet'), MiddlewareHandler = require('concierge/middleware'); class Platform extends MiddlewareHandler { constructor (bypassInit) { super(); this.bypassInit = bypassInit; this.defaultPrefix = '/'; this.packageInfo = require(global.rootPathJoin('package.json')); this.modulesLoader = new (require(global.rootPathJoin('core/modules/modules.js')))(this); this.statusFlag = global.StatusFlag.NotStarted; this.onShutdown = null; this.waitingTime = 250; this.packageInfo.name = this.packageInfo.name.toProperCase(); this.config = require(global.rootPathJoin('core/modules/config.js')); this.loopbackBuilder = require(global.rootPathJoin('core/modules/loopback.js'))(this); this.modulesLoader.on('loadSystem', this._loadSystemConfig.bind(this)); global.shim = require(global.rootPathJoin('core/modules/shim.js')); this._boundErrorHandler = err => { global.currentPlatform._errorHandler.call(global.currentPlatform, err); }; this.onMessage = this.onMessage.bind(this); process.on('uncaughtException', this._boundErrorHandler); process.on('unhandledRejection', this._boundErrorHandler); } _errorHandler (err, api, event) { const blame = global.getBlame(null, null, err) || '!CORE!'; let message; if (api && event) { const part = `"${event.body}" (${blame})`; message = $$`${part} failed ${event.sender_name} caused it`; this.runMiddleware('error', api.sendMessage, message, event.thread_id); } else { const part = `"${blame}"`; message = $$`${part} failed ${'<unknown>'} caused it`; } console.error(message); console.critical(err); this.emitAsync('uncaughtError', err, blame, api, event); } _handleTransaction (module, args) { let returnVal = null; const timeout = setTimeout(() => { if (returnVal !== null) { return; } args[0].sendTyping(args[1].thread_id); }, this.waitingTime); try { returnVal = this.runMiddlewareSync.apply(this, ['run', module.run.bind(module)].concat(args)); } catch (e) { this._errorHandler(e, args[0], args[1]); } finally { clearTimeout(timeout); } return returnVal; } _handleMessage (api, event) { const matchArgs = [event, api.commandPrefix], runArgs = [api, event], loadedModules = this.modulesLoader.getLoadedModules('module'); event.module_match_count = 0; for (let lm of loadedModules) { let matchResult; try { matchResult = this.runMiddlewareSync.apply(this, ['match', lm.match.bind(lm)].concat(matchArgs, lm.__descriptor)); } catch (e) { console.error($$`BrokenModule ${lm.__descriptor.name}`); console.critical(e); continue; } if (matchResult) { event.module_match_count++; const transactionRes = this._handleTransaction(lm, runArgs); if (event.shouldAbort || transactionRes) { return; } } } this.emitAsync('message', api, event); } /** * Callback for integrations, for passing messages to integrations. * @param {Object} api the api that raised the message. * @param {Object} event the event that occured. */ onMessage (api, event) { const loopBack = this.loopbackBuilder(api, event); this.runMiddleware('before', this._handleMessage, loopBack.api, loopBack.event); } /** * Gets all of the started integration APIs as a key-value pair object. * @return {Object} a key-value pair object of integrations. */ getIntegrationApis () { const integs = this.modulesLoader.getLoadedModules('integration'), apis = {}; for (let integ of integs.filter(i => !!i.__running)) { apis[integ.__descriptor.name] = integ.getApi(); } return apis; } /** * Gets a loaded module by name or filter function. * @param {string|function()} arg either the name or a filter function to find the module. * @return {Object} the module (if found), array-like otherwise. * @emits Platform#started */ getModule (arg) { const modules = this.modulesLoader.getLoadedModules('module'); let func = arg; if (typeof(func) !== 'function') { func = mod => mod.__descriptor.name.trim().toLowerCase() === arg.trim().toLowerCase(); } return modules.find(func); } _loadSystemConfig () { console.warn($$`LoadingSystemConfig`); $$.setLocale(this.config.getSystemConfig('i18n').locale); const firstRun = this.config.getSystemConfig('firstRun'); if (!firstRun.hasRun) { firstRun.hasRun = true; require(global.rootPathJoin('core/modules/firstRun.js'))(this.bypassInit, this.config, this.modulesLoader); } } /** * Start Concierge, load modules and start integrations. * @param {Array<string>} integrations list of integrations to start. * @param {Array<string>} modules optional list of modules to load. * @emits Platform#started */ start (integrations, modules) { if (this.statusFlag !== global.StatusFlag.NotStarted) { throw new Error($$`StartError`); } console.title(`\n${figlet.textSync(this.packageInfo.name.toProperCase())}` + `\n ${this.packageInfo.version}\n------------------------------------`); console.warn($$`StartingSystem`); // Load modules console.warn($$`LoadingModules`); this.modulesLoader.loadAllModules(modules); console.warn($$`StartingIntegrations`); for (let integration of integrations) { try { console.info($$`Loading integration '${integration}'...\t`); this.modulesLoader.startIntegration(this.onMessage, integration); } catch (e) { if (e.message === 'Cannot find integration to start') { console.error(`Unknown integration '${integration}'`); } else { console.error($$`Failed to start output integration '${integration}'.`); console.critical(e); } } } this.statusFlag = global.StatusFlag.Started; console.warn($$`SystemStarted` + ' ' + $$`HelloWorld`.rainbow); this.heartBeat = setInterval(() => console.debug('Core Heartbeat'), 2147483647); this.emitAsync('started'); } /** * Shutdown Concierge. * @param {Symbol(string)} flag shutdown status. Defaults to `global.StatusFlag.Shutdown`. * @emits Platform#preshutdown * @emits Platform#shutdown * @see `global.StatusFlag` */ shutdown (flag) { if (this.statusFlag !== global.StatusFlag.Started) { throw new Error($$`ShutdownError`); } this.emit('preshutdown'); if (!flag) { flag = global.StatusFlag.Unknown; } // Unload user modules this.config.saveConfig(); this.modulesLoader.unloadAllModules(); this.statusFlag = flag || global.StatusFlag.Shutdown; process.removeListener('uncaughtException', this._boundErrorHandler); process.removeListener('unhandledRejection', this._boundErrorHandler); console.warn($$`${this.packageInfo.name} Shutdown`); clearInterval(this.heartBeat); this.emit('shutdown', this.statusFlag); } } module.exports = Platform;