/**
* The middleware chain is in charge of pre-processing incoming and outgoing messages.
* A middleware can for example translate a message on receiving and before sending.
* Most middleware are registered by the modules. For example, the Analytics module
* keeps track of the messages with the help of an incoming and an outgoing middleware.
* @namespace Middleware
*/
import _ from 'lodash'
import mware from 'mware'
import path from 'path'
import fs from 'fs'
import Promise from 'bluebird'
const createMiddleware = (bp, middlewareName) => {
const _use = mware()
const _error = mware()
const use = middleware => {
if (typeof middleware !== 'function') {
throw new TypeError('Expected all middleware arguments to be functions')
}
if (middleware.length === 2) {
_use(middleware)
} else if (middleware.length === 3) {
_error(middleware)
}
}
const dispatch = event => {
if (!_.isPlainObject(event)) {
throw new TypeError('Expected all dispatch arguments to be plain event objects')
}
const conformity = {
type: value => typeof value === 'string',
platform: value => typeof value === 'string',
text: value => typeof value === 'string',
raw: () => true
}
if (!_.conformsTo(event, conformity)) {
throw new TypeError(
'Expected event to contain (type: string), ' + '(platform: string), (text: string), (raw: any)'
)
}
// Provide botpress to the event handlers
event.bp = bp
_use.run(event, err => {
if (err) {
_error.run(err, event, () => {
bp.logger.error(`[BOTPRESS] Unhandled error in middleware (${middlewareName}). Error: ${err.message}`)
})
}
})
return event._promise || Promise.resolve()
}
return { use, dispatch }
}
module.exports = (bp, dataLocation, projectLocation, logger) => {
const middlewaresFilePath = path.join(dataLocation, 'middlewares.json')
const noopChain = arg => {
let message =
'Middleware called before middlewares have been loaded. This is a no-op.' +
' Have you forgotten to call `bp.loadMiddlewares()` in your bot?'
if (arg && typeof arg === 'object') {
message += '\nCalled with: ' + JSON.stringify(arg, null, 2)
}
logger.warn(message)
}
const readCustomizations = () => {
if (!fs.existsSync(middlewaresFilePath)) {
fs.writeFileSync(middlewaresFilePath, '{}')
}
return JSON.parse(fs.readFileSync(middlewaresFilePath))
}
let incoming = noopChain,
outgoing = noopChain,
customizations = readCustomizations()
const middlewares = []
const writeCustomizations = () => {
fs.writeFileSync(middlewaresFilePath, JSON.stringify(customizations))
}
const setCustomizations = middlewares => {
_.each(middlewares, middleware => {
const { name, order, enabled } = middleware
customizations[name] = { order, enabled }
})
writeCustomizations()
}
const resetCustomizations = () => {
customizations = {}
writeCustomizations()
}
/**
* @typedef {Object} Event
* @prop {String} platform
* @prop {String} text
* @prop {object} raw
* @prop {String} type
* @memberOf! Middleware
*/
/**
* @callback Handler
* @memberOf! Middleware
* @param {Object} event The incoming or outgoing event
* @param {Function} next Call this function to make the event flow to the next middleware (see example)
*/
/**
* @typedef {Object} Middleware
* @memberOf! Middleware
* @property {String} name Unique name of the middleware
* @property {Middleware.Handler} handler The handler function
* @property {String} type Can be 'incoming' or 'outgoing'
* @property {Number} order A positive number from 0 (before everything else) to 1000 (last middleware)
* @property {Boolean} [enabled=true] Whether this middleware is enabled or not
*/
/**
* Registers a new middleware into the chain
* @param {Middleware.Middleware} middleware The middleware to register
* @memberOf! Middleware
*/
const register = middleware => {
if (!middleware || !middleware.name) {
logger.error('A unique middleware name is mandatory')
return false
}
if (!middleware.handler) {
logger.error('A middleware handler is mandatory')
return false
}
if (!middleware.type || (middleware.type !== 'incoming' && middleware.type !== 'outgoing')) {
logger.error('A middleware type (incoming or outgoing) is required')
return false
}
middleware.order = middleware.order || 0
middleware.enabled = typeof middleware.enabled === 'undefined' ? true : !!middleware.enabled
if (_.some(middlewares, m => m.name === middleware.name)) {
logger.error('Another middleware with the same name has already been registered')
return false
}
middlewares.push(middleware)
}
const list = () => {
return _.orderBy(
middlewares.map(middleware => {
const customization = customizations[middleware.name]
if (customization) {
return Object.assign({}, middleware, customization)
}
return middleware
}),
'order'
)
}
const load = () => {
incoming = createMiddleware(bp, 'incoming')
outgoing = createMiddleware(bp, 'outgoing')
const { middleware: licenseMiddleware } = bp.licensing
incoming.use(licenseMiddleware)
_.each(list(), m => {
if (!m.enabled) {
return logger.debug('SKIPPING middleware:', m.name, ' [Reason=disabled]')
}
logger.debug('Loading middleware:', m.name)
if (m.type === 'incoming') {
incoming.use(m.handler)
} else {
outgoing.use(m.handler)
}
})
}
const sendToMiddleware = type => event => {
const mw = type === 'incoming' ? incoming : outgoing
return mw.dispatch ? mw.dispatch(event) : mw(event)
}
return {
load,
list,
register,
/**
* Sends an incoming event (from the user to the bot)
* @param {Middleware.Event} event An event object
* @memberOf! Middleware
*/
sendIncoming: event => bp.messages.in.enqueue(event),
/**
* Sends an outgoing event (from the bot to the user)
* @param {Middleware.Event} event An event object
* @memberOf! Middleware
*/
sendOutgoing: event => bp.messages.out.enqueue(event),
sendIncomingImmediately: sendToMiddleware('incoming'),
sendOutgoingImmediately: sendToMiddleware('outgoing'),
getCustomizations: () => customizations,
setCustomizations,
resetCustomizations
}
}