UNPKG

botpress

Version:

The world's first CMS for bots. Easily create, manage and extend chatbots.

658 lines (493 loc) 23.4 kB
'use strict'; require('source-map-support/register'); var _chalk = require('chalk'); var _chalk2 = _interopRequireDefault(_chalk); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs); var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash); var _cluster = require('cluster'); var _cluster2 = _interopRequireDefault(_cluster); var _dotenv = require('dotenv'); var _dotenv2 = _interopRequireDefault(_dotenv); var _ms = require('ms'); var _ms2 = _interopRequireDefault(_ms); var _opn = require('opn'); var _opn2 = _interopRequireDefault(_opn); var _middlewares = require('./middlewares'); var _middlewares2 = _interopRequireDefault(_middlewares); var _logger = require('./logger'); var _logger2 = _interopRequireDefault(_logger); var _security = require('./security'); var _security2 = _interopRequireDefault(_security); var _notifications = require('./notifications'); var _notifications2 = _interopRequireDefault(_notifications); var _hear = require('./hear'); var _hear2 = _interopRequireDefault(_hear); var _fallback = require('./fallback'); var _fallback2 = _interopRequireDefault(_fallback); var _database = require('./database'); var _database2 = _interopRequireDefault(_database); var _ghostContent = require('./ghost-content'); var _ghostContent2 = _interopRequireDefault(_ghostContent); var _mediaManager = require('./media-manager'); var _mediaManager2 = _interopRequireDefault(_mediaManager); var _licensing = require('./licensing'); var _licensing2 = _interopRequireDefault(_licensing); var _about = require('./about'); var _about2 = _interopRequireDefault(_about); var _modules = require('./modules'); var _modules2 = _interopRequireDefault(_modules); var _cloud = require('./cloud'); var _cloud2 = _interopRequireDefault(_cloud); var _renderers = require('./renderers'); var _renderers2 = _interopRequireDefault(_renderers); var _users = require('./users'); var _users2 = _interopRequireDefault(_users); var _service = require('./content/service'); var _service2 = _interopRequireDefault(_service); var _getItemProviders = require('./content/getItemProviders'); var _getItemProviders2 = _interopRequireDefault(_getItemProviders); var _helpers = require('./helpers'); var _helpers2 = _interopRequireDefault(_helpers); var _janitor = require('./janitor'); var _janitor2 = _interopRequireDefault(_janitor); var _stats = require('./stats'); var _stats2 = _interopRequireDefault(_stats); var _bus = require('./bus'); var _bus2 = _interopRequireDefault(_bus); var _configManager = require('./config-manager'); var _configManager2 = _interopRequireDefault(_configManager); var _provider = require('./dialog/provider'); var _provider2 = _interopRequireDefault(_provider); var _state = require('./dialog/state'); var _state2 = _interopRequireDefault(_state); var _engine = require('./dialog/engine'); var _engine2 = _interopRequireDefault(_engine); var _processors = require('./dialog/processors'); var _processors2 = _interopRequireDefault(_processors); var _janitor3 = require('./dialog/janitor'); var _janitor4 = _interopRequireDefault(_janitor3); var _skills = require('./skills'); var _skills2 = _interopRequireDefault(_skills); var _memory = require('./queues/memory'); var _memory2 = _interopRequireDefault(_memory); var _crypto = require('./security/crypto'); var _crypto2 = _interopRequireDefault(_crypto); var _package = require('../package.json'); var _package2 = _interopRequireDefault(_package); var _server = require('./server'); var _server2 = _interopRequireDefault(_server); var _util = require('./util'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /** * The global instance of Botpress, which is the main object * your bot will use to interact with Botpress. * @var {Botpress} bp * @example * // File: index.js * // All bots are passed an instance of `bp` upon start * // This is an example of an empty bot * module.exports = (bp) => { ... } */ /** * @namespace Botpress * @property {DialogEngine} dialogEngine APIs to create and manipulate conversation flows * @property {KVS} kvs Convenient, high-level storage mechanism * @property {ContentManager} contentManager APIs to manage the content programmatically * @property {ContentRenderer} renderers Change the look and feel of the * Content Elements (messages) on the different channels * @property {Database} db (Advanced) Access to the internal Botpress Database * @property {Users} users Store and manipulate data about users * @property {DialogStateManager} dialogEngine.stateManager APIs to manipulate conversation states * @property {Logger} logger Logging utility * @property {Botfile} botfile The current botfile of the running bot */ const RESTART_EXIT_CODE = 107; const mkdirIfNeeded = (path, logger) => { if (!_fs2.default.existsSync(path)) { logger.info(`Creating data directory: ${path}`); try { _fs2.default.mkdirSync(path); } catch (err) { logger.error(`[FATAL] Error creating directory: ${err.message}`); process.exit(1); } } }; const REQUIRED_PROPS = ['botUrl']; const validateBotfile = botfile => { if ('disableFileLogs' in botfile || _lodash2.default.get(botfile, 'log.file')) { console.log(` You're using the old logs configuration format. Since v11 botpress has stopped storing logs in files and has moved them to the database. Please update your botfile. Old configuration format: /* By default logs are enabled and available in dataDir */ disableFileLogs: false, log: { file: 'bot.log', maxSize: 1e6 // 1mb } New format: /* By default logs are enabled and stored in the DB for 30 days */ logs: { enabled: true, keepDays: 30 } `); throw new Error('Outdated botfile format'); } (0, _util.validateBotVersion)(_package2.default.version, botfile.version); for (const prop of REQUIRED_PROPS) { if (!(prop in botfile)) { throw new Error(`Missing required botpress setting: ${prop}`); } } }; class botpress { constructor({ botfile, options = {} }) { this.start = () => { if (_cluster2.default.isMaster) { let firstWorkerHasStartedAlready = false; const quit = (code = 0) => { if (this.stopServer) { this.stopServer(); } process.exit(code); }; const receiveMessageFromWorker = message => { if (message && message.workerStatus === 'starting') { if (!firstWorkerHasStartedAlready) { firstWorkerHasStartedAlready = true; } else { (0, _util.print)('info', '*** restarted worker process ***'); this.stats.track('bot', 'restarted'); } } else if (message.type === 'exit') { quit(); } }; _cluster2.default.on('exit', (worker, code /* , signal */) => { if (code === RESTART_EXIT_CODE) { _cluster2.default.fork().on('message', receiveMessageFromWorker); } else { quit(code); } }); if (!this.botfile.disableClusterMode) { _cluster2.default.fork().on('message', receiveMessageFromWorker); } } if (_cluster2.default.isWorker) { process.send && process.send({ workerStatus: 'starting' }); this._start().catch(err => { (0, _util.print)('error', 'Error starting botpress: ', err.message, err.stack); }); } }; this.version = (0, _util.getBotpressVersion)(); /** * The project location, which is the folder where botfile.js located */ this.projectLocation = _lodash2.default.isString(botfile) ? _path2.default.dirname(botfile) : _path2.default.resolve('.'); /** * Setup env with dotenv *before* requiring the botfile config */ this._setupEnv(); /** * The botfile config object */ this.botfile = _lodash2.default.isString(botfile) ? require(botfile) : botfile; validateBotfile(this.botfile); this.stats = (0, _stats2.default)(this.botfile); this.interval = null; /* Check --inspect flag */ const opts = _lodash2.default.result(options, 'opts') || {}; this.hasInspectMode = opts.inspect || opts.i; } /** * Start the bot instance * * It will do the following initiation steps: * * 1. setup logger * 2. resolve paths (dataLocation) * 3. inject security functions * 4. load modules * @private */ _start() { var _this = this; return _asyncToGenerator(function* () { _this.stats.track('bot', 'started'); if (!_this.interval) { _this.interval = setInterval(function () { _this.stats.track('bot', 'running'); }, 30 * 1000); } _this.botpressPath = _path2.default.join(__dirname, '../'); const { projectLocation, botfile } = _this; const isFirstRun = _fs2.default.existsSync(_path2.default.join(projectLocation, '.welcome')); const dataLocation = (0, _util.getDataLocation)(botfile.dataDir, projectLocation); const configLocation = (0, _util.getDataLocation)(botfile.modulesConfigDir, projectLocation); const dbLocation = _path2.default.join(dataLocation, 'db.sqlite'); const version = _package2.default.version; const logger = (0, _logger2.default)(botfile.logs); mkdirIfNeeded(dataLocation, logger); mkdirIfNeeded(configLocation, logger); const db = (0, _database2.default)({ sqlite: { location: dbLocation }, postgres: botfile.postgres, logger, botpressPath: _this.botpressPath }); yield db.get(); // Running migrations const janitor = (0, _janitor2.default)({ db, logger }); logger.enableDbStorageIfNeeded({ db, janitor }); logger.info(`Starting botpress version ${version}`); janitor.start(); const kvs = db._kvs; const cloud = yield (0, _cloud2.default)({ projectLocation, botfile, logger }); if (!!botfile.login.useCloud && (yield cloud.isPaired())) { setInterval(function () { return cloud.updateRemoteEnv(); }, (0, _ms2.default)('10m')); cloud.updateRemoteEnv(); // async on purpose } const configManager = new _configManager2.default({ configLocation, botfile, logger }); const security = yield (0, _security2.default)({ dataLocation, securityConfig: botfile.login, projectLocation, db, cloud, logger }); const modules = (0, _modules2.default)(logger, projectLocation, dataLocation, configManager); const moduleDefinitions = modules._scan(); const events = new _bus2.default(); const notifications = (0, _notifications2.default)({ knex: yield db.get(), modules: moduleDefinitions, logger, events }); const about = (0, _about2.default)(projectLocation); const licensing = (0, _licensing2.default)({ logger, projectLocation, version, db, botfile, bp: _this }); const middlewares = (0, _middlewares2.default)(_this, dataLocation, projectLocation, logger); const { hear, middleware: hearMiddleware } = (0, _hear2.default)(); const { middleware: fallbackMiddleware } = (0, _fallback2.default)(_this); const users = (0, _users2.default)({ db }); const ghostManager = (0, _ghostContent2.default)({ projectLocation, logger, db, enabled: !!_lodash2.default.get(botfile, 'ghostContent.enabled') }); const contentManager = yield (0, _service2.default)({ logger, projectLocation, botfile, ghostManager }); const mediaManager = yield (0, _mediaManager2.default)({ botfile, logger, ghostManager, projectLocation }); // Register the built-in item providers such as "-random()" Object.keys(_getItemProviders2.default).forEach(function (provider) { contentManager.registerGetItemProvider(provider, _getItemProviders2.default[provider]); }); const renderers = (0, _renderers2.default)({ logger, middlewares, db, contentManager, botfile }); const stateManager = (0, _state2.default)({ db }); const flowProvider = new _provider2.default({ logger, projectLocation, botfile, ghostManager }); const dialogJanitor = (0, _janitor4.default)({ db, middlewares, botfile }); const dialogEngine = new _engine2.default({ flowProvider, stateManager, logger }); const crypto = new _crypto2.default({ botfile }); const skillsManager = new _skills2.default({ logger }); dialogEngine.onError(function ({ message }) { return notifications.create({ message: `DialogEngine: ${message}`, level: 'error', redirectUrl: '/logs' }); }); // Registers the default output processor, which sends messages to the user dialogEngine.registerOutputProcessor(_processors2.default['default']); dialogJanitor.install(); const incomingQueue = new _memory2.default('Incoming', logger, { redis: botfile.redis }); incomingQueue.subscribe(function (job) { return middlewares.sendIncomingImmediately(job.event); }); const outgoingQueue = new _memory2.default('Outgoing', logger, { redis: botfile.redis }); outgoingQueue.subscribe(function (job) { return middlewares.sendOutgoingImmediately(job.event); }); const messages = { in: { enqueue: function (event) { return incomingQueue.enqueue({ event }); }, cancelAll: function (event) { return incomingQueue.cancelAll({ event }); }, peek: function (event) { return incomingQueue.peek({ event }); } }, out: { enqueue: function (event) { return outgoingQueue.enqueue({ event }); }, cancelAll: function (event) { return outgoingQueue.cancelAll({ event }); }, peek: function (event) { return outgoingQueue.peek({ event }); } } }; middlewares.register(renderers.incomingMiddleware); middlewares.register(hearMiddleware); middlewares.register(fallbackMiddleware); _lodash2.default.assign(_this, { dataLocation, isFirstRun, version, logger, security, // login, authenticate, getSecret events, notifications, // load, save, send about, middlewares, hear, licensing, modules, db, janitor, kvs, configManager, cloud, renderers, users, ghostManager, contentManager, mediaManager, dialogEngine, dialogJanitor, messages, crypto, skills: skillsManager }); Object.defineProperty(_this, 'umm', { get() { logger.warn('DEPRECATION NOTICE – bp.umm is deprecated and will be removed – Please see bp.renderers instead.'); return renderers; } }); const loadedModules = yield modules._load(moduleDefinitions, _this); const dbModulesMigration = yield _this.db.migration(db, moduleDefinitions); yield dbModulesMigration.up(); _this.stats.track('bot', 'modules', 'loaded', loadedModules.length); _lodash2.default.assign(_this, { _loadedModules: loadedModules }); skillsManager.registerSkillsFromModules(_lodash2.default.values(loadedModules)); yield contentManager.init(); notifications._bindEvents(); const server = (0, _server2.default)(_this); server.start().then(function (srv) { _this.stopServer = srv && srv.stop; if (_this.hasInspectMode) { const serverPID = process.pid; const inspectSignal = 'SIGUSR1'; if (process.platform === 'win32') { process._debugProcess(serverPID); } else { process.kill(serverPID, inspectSignal); } } events.emit('ready'); for (const mod of _lodash2.default.values(loadedModules)) { mod.handlers.ready && mod.handlers.ready(_this, mod.configuration, _helpers2.default); } const { botUrl } = botfile; logger.info(_chalk2.default.green.bold(`Bot launched. Visit: ${botUrl}`)); if (_util.isDeveloping) { (0, _opn2.default)(botUrl, { wait: false }); } }); const middlewareAutoLoading = _lodash2.default.get(botfile, 'middleware.autoLoading'); if (!_lodash2.default.isNil(middlewareAutoLoading) && middlewareAutoLoading === false) { logger.debug('Middleware Auto Loading was disabled. Call bp.middlewares.load() manually.'); } else { middlewares.load(); } const projectEntry = require(projectLocation); if (typeof projectEntry === 'function') { projectEntry.call(projectEntry, _this); } else { logger.error('[FATAL] The bot entry point must be a function that takes an instance of bp'); process.exit(1); } process.on('uncaughtException', function (err) { logger.error('[FATAL] An unhandled exception occurred in your bot', err); if (_util.isDeveloping) { logger.error(err.stack); } _this.stats.trackException(err.message); process.exit(1); }); process.on('unhandledRejection', function (reason, p) { logger.error('Unhandled Rejection in Promise: ', p, 'Reason:', reason); _this.stats.trackException(reason); if (_util.isDeveloping && reason && reason.stack) { logger.error(reason.stack); } }); })(); } restart(interval = 0) { setTimeout(() => { process.exit(RESTART_EXIT_CODE); }, interval); } _setupEnv() { const envPath = _path2.default.resolve(this.projectLocation, '.env'); if (_fs2.default.existsSync(envPath)) { const envConfig = _dotenv2.default.parse(_fs2.default.readFileSync(envPath)); for (const k in envConfig) { if (_lodash2.default.isNil(process.env[k]) || process.env.ENV_OVERLOAD) { process.env[k] = envConfig[k]; } } } } } module.exports = botpress; //# sourceMappingURL=botpress.js.map