/**
* 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
*/
import 'source-map-support/register'
import chalk from 'chalk'
import path from 'path'
import fs from 'fs'
import _ from 'lodash'
import cluster from 'cluster'
import dotenv from 'dotenv'
import ms from 'ms'
import EventBus from './bus'
import createMiddlewares from './middlewares'
import createLogger from './logger'
import createSecurity from './security'
import createNotifications from './notifications'
import createHearMiddleware from './hear'
import createFallbackMiddleware from './fallback'
import createDatabase from './database'
import createGhostManager from './ghost-content'
import createMediaManager from './media-manager'
import createLicensing from './licensing'
import createAbout from './about'
import createModules from './modules'
import createCloud from './cloud'
import createRenderers from './renderers'
import createUsers from './users'
import createContentManager from './content/service'
import defaultGetItemProviders from './content/getItemProviders'
import FlowProvider from './dialog/provider'
import StateManager from './dialog/state'
import DialogEngine from './dialog/engine'
import DialogProcessors from './dialog/processors'
import DialogJanitor from './dialog/janitor'
import SkillsManager from './skills'
import createHelpers from './helpers'
import stats from './stats'
import Queue from './queues/memory'
import packageJson from '../package.json'
import createServer from './server'
import { getDataLocation, getBotpressVersion } from './util'
import { isDeveloping, print } from './util'
const RESTART_EXIT_CODE = 107
const mkdirIfNeeded = (path, logger) => {
if (!fs.existsSync(path)) {
logger.info(`Creating data directory: ${path}`)
try {
fs.mkdirSync(path)
} catch (err) {
logger.error(`[FATAL] Error creating directory: ${err.message}`)
process.exit(1)
}
}
}
const REQUIRED_PROPS = ['botUrl']
class botpress {
constructor({ botfile }) {
this.version = getBotpressVersion()
/**
* The project location, which is the folder where botfile.js located
*/
this.projectLocation = path.dirname(botfile)
/**
* Setup env with dotenv *before* requiring the botfile config
*/
this._setupEnv()
/**
* The botfile config object
*/
// eslint-disable-next-line no-eval
this.botfile = eval('require')(botfile)
for (const prop of REQUIRED_PROPS) {
if (!(prop in this.botfile)) {
throw new Error(`Missing required botpress setting: ${prop}`)
}
}
this.stats = stats(this.botfile)
this.interval = null
}
/**
* 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
*/
async _start() {
this.stats.track('bot', 'started')
if (!this.interval) {
this.interval = setInterval(() => {
this.stats.track('bot', 'running')
}, 30 * 1000)
}
// change the current working directory to botpress's installation path
// the bot's location is kept in this.projectLocation
process.chdir(path.join(__dirname, '../'))
const { projectLocation, botfile } = this
const isFirstRun = fs.existsSync(path.join(projectLocation, '.welcome'))
const dataLocation = getDataLocation(botfile.dataDir, projectLocation)
const modulesConfigDir = getDataLocation(botfile.modulesConfigDir, projectLocation)
const dbLocation = path.join(dataLocation, 'db.sqlite')
const version = packageJson.version
const logger = createLogger(dataLocation, botfile.log)
mkdirIfNeeded(dataLocation, logger)
mkdirIfNeeded(modulesConfigDir, logger)
logger.info(`Starting botpress version ${version}`)
const db = createDatabase({
sqlite: { location: dbLocation },
postgres: botfile.postgres,
logger
})
const kvs = db._kvs
const cloud = await createCloud({ projectLocation, botfile, logger })
if (!!botfile.login.useCloud && (await cloud.isPaired())) {
setInterval(() => cloud.updateRemoteEnv(), ms('10m'))
cloud.updateRemoteEnv() // async on purpose
}
const security = await createSecurity({
dataLocation,
securityConfig: botfile.login,
projectLocation,
db,
cloud,
logger
})
const modules = createModules(logger, projectLocation, dataLocation, kvs)
const moduleDefinitions = modules._scan()
const events = new EventBus()
const notifications = createNotifications({
knex: await db.get(),
modules: moduleDefinitions,
logger,
events
})
const about = createAbout(projectLocation)
const licensing = createLicensing({
logger,
projectLocation,
version,
db,
botfile,
bp: this
})
const middlewares = createMiddlewares(this, dataLocation, projectLocation, logger)
const { hear, middleware: hearMiddleware } = createHearMiddleware()
const { middleware: fallbackMiddleware } = createFallbackMiddleware(this)
const users = createUsers({ db })
const ghostManager = createGhostManager({
projectLocation,
logger,
db,
enabled: !!_.get(botfile, 'ghostContent.enabled')
})
const contentManager = await createContentManager({
logger,
projectLocation,
botfile,
ghostManager
})
const mediaManager = await createMediaManager({
botfile,
logger,
ghostManager,
projectLocation
})
// Register the built-in item providers such as "-random()"
Object.keys(defaultGetItemProviders).forEach(provider => {
contentManager.registerGetItemProvider(provider, defaultGetItemProviders[provider])
})
const renderers = createRenderers({
logger,
middlewares,
db,
contentManager,
botfile
})
const stateManager = StateManager({ db })
const flowProvider = new FlowProvider({ logger, projectLocation, botfile, ghostManager })
const dialogJanitor = new DialogJanitor({ db, middlewares, botfile })
const dialogEngine = new DialogEngine({ flowProvider, stateManager, logger })
const skillsManager = new SkillsManager({ logger })
dialogEngine.onError(({ message }) =>
notifications.create({ message: `DialogEngine: ${message}`, level: 'error', redirectUrl: '/logs' })
)
// Registers the default output processor, which sends messages to the user
dialogEngine.registerOutputProcessor(DialogProcessors['default'])
dialogJanitor.install()
const incomingQueue = new Queue('Incoming', logger, {
redis: botfile.redis
})
incomingQueue.subscribe(job => middlewares.sendIncomingImmediately(job.event))
const outgoingQueue = new Queue('Outgoing', logger, {
redis: botfile.redis
})
outgoingQueue.subscribe(job => middlewares.sendOutgoingImmediately(job.event))
const messages = {
in: {
enqueue: event => incomingQueue.enqueue({ event }),
cancelAll: event => incomingQueue.cancelAll({ event }),
peek: event => incomingQueue.peek({ event })
},
out: {
enqueue: event => outgoingQueue.enqueue({ event }),
cancelAll: event => outgoingQueue.cancelAll({ event }),
peek: event => outgoingQueue.peek({ event })
}
}
middlewares.register(renderers.incomingMiddleware)
middlewares.register(hearMiddleware)
middlewares.register(fallbackMiddleware)
_.assign(this, {
dataLocation,
isFirstRun,
version,
logger,
security, // login, authenticate, getSecret
events,
notifications, // load, save, send
about,
middlewares,
hear,
licensing,
modules,
db,
kvs,
cloud,
renderers,
get umm() {
logger.warn(
'DEPRECATION NOTICE – bp.umm is deprecated and will be removed in `botpress@3.0` – Please see bp.renderers instead.'
)
return renderers
},
users,
ghostManager,
contentManager,
mediaManager,
dialogEngine,
dialogJanitor,
messages,
skills: skillsManager
})
const loadedModules = await modules._load(moduleDefinitions, this)
this.stats.track('bot', 'modules', 'loaded', loadedModules.length)
_.assign(this, {
_loadedModules: loadedModules
})
skillsManager.registerSkillsFromModules(_.values(loadedModules))
await contentManager.init()
notifications._bindEvents()
const server = createServer(this)
server.start().then(srv => {
this.stopServer = srv && srv.stop
events.emit('ready')
for (const mod of _.values(loadedModules)) {
mod.handlers.ready && mod.handlers.ready(this, mod.configuration, createHelpers)
}
const { botUrl } = botfile
logger.info(chalk.green.bold(`Bot launched. Visit: ${botUrl}`))
})
const middlewareAutoLoading = _.get(botfile, 'middleware.autoLoading')
if (!_.isNil(middlewareAutoLoading) && middlewareAutoLoading === false) {
logger.debug('Middleware Auto Loading was disabled. Call bp.middlewares.load() manually.')
} else {
middlewares.load()
}
// eslint-disable-next-line no-eval
const projectEntry = eval('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', err => {
logger.error('[FATAL] An unhandled exception occurred in your bot', err)
if (isDeveloping) {
logger.error(err.stack)
}
this.stats.trackException(err.message)
process.exit(1)
})
process.on('unhandledRejection', (reason, p) => {
logger.error('Unhandled Rejection in Promise: ', p, 'Reason:', reason)
this.stats.trackException(reason)
if (isDeveloping && reason && reason.stack) {
logger.error(reason.stack)
}
})
}
start = () => {
if (cluster.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 {
print('info', '*** restarted worker process ***')
this.stats.track('bot', 'restarted')
}
} else if (message.type === 'exit') {
quit()
}
}
cluster.on('exit', (worker, code /* , signal */) => {
if (code === RESTART_EXIT_CODE) {
cluster.fork().on('message', receiveMessageFromWorker)
} else {
quit(code)
}
})
cluster.fork().on('message', receiveMessageFromWorker)
}
if (cluster.isWorker) {
process.send({ workerStatus: 'starting' })
this._start().catch(err => {
print('error', 'Error starting botpress: ', err.message, err.stack)
})
}
}
restart(interval = 0) {
setTimeout(() => {
process.exit(RESTART_EXIT_CODE)
}, interval)
}
_setupEnv() {
const envPath = path.resolve(this.projectLocation, '.env')
if (fs.existsSync(envPath)) {
const envConfig = dotenv.parse(fs.readFileSync(envPath))
for (const k in envConfig) {
if (_.isNil(process.env[k]) || process.env.ENV_OVERLOAD) {
process.env[k] = envConfig[k]
}
}
}
}
}
module.exports = botpress