botpress
Version:
The world's first CMS for bots. Easily create, manage and extend chatbots.
658 lines (493 loc) • 23.4 kB
JavaScript
;
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