node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram**, Facebook Messenger and Slack. Almost no coding skills required
608 lines (560 loc) • 19.3 kB
JavaScript
var _ = require('underscore');
var clc = require('cli-color');
var prettyjson = require('prettyjson');
var utils = require('../helpers/utils');
var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
var lcd = require('../helpers/lcd');
var when = utils.when;
var green = clc.greenBright;
var white = clc.white;
//var grey = clc.blackBright;
var yellow = clc.yellow;
//var notice = clc.blue;
var red = clc.red;
var orange = clc.xterm(214);
var grey = clc.blackBright;
var ChatExpress = function(options) {
var _this = this;
this.options = _.extend({
contextProvider: null,
connector: null,
inboundMessage: null,
transport: null,
chatIdKey: null,
userIdKey: null,
tsKey: null,
debug: true,
onStart: null,
onStop: null,
RED: null,
routes: null,
routesDescription: null,
events: null,
relaxChatId: false
}, options);
this.ins = [];
this.outs = [];
this.uses = [];
// configuration warnings
if (_.isEmpty(this.options.chatIdKey) && !_.isFunction(this.options.chatIdKey)) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: chatIdKey option is empty'));
}
if (_.isEmpty(this.options.userIdKey) && !_.isFunction(this.options.userIdKey)) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: userIdKey option is empty'));
}
if (_.isEmpty(this.options.transport)) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: transport option is empty'));
}
function evaluateParam(payload, newKey, optionKey) {
var options = _this.options;
if (options[optionKey] != null) {
if (_.isString(options[optionKey]) && newKey != options[optionKey]) {
payload[newKey] = payload[options[optionKey]];
delete payload[options[optionKey]];
} else if (_.isFunction(options[optionKey])) {
payload[newKey] = options[optionKey](payload);
}
}
}
function parseMessage(payload, options) {
payload = _.clone(payload);
// sets inbound
payload.inbound = true;
// sets the transport
if (!_.isEmpty(_this.options.transport)) {
payload.transport = _this.options.transport;
}
evaluateParam(payload, 'chatId', 'chatIdKey');
evaluateParam(payload, 'userId', 'userIdKey');
evaluateParam(payload, 'ts', 'tsKey');
evaluateParam(payload, 'type', 'type');
evaluateParam(payload, 'language', 'language');
// at this point should have at least the values chatId and type
if (payload.chatId == null && !options.relaxChatId) {
throw 'Error: inbound message key "chatId" for transport ' + _this.options.transport + ' is empty\n\n'
+ JSON.stringify(payload);
}
/*if (payload.type == null) {
throw 'Error: inbound message key "type" for transport ' + _this.options.transport + ' is empty\n\n'
+ JSON.stringify(payload);
}*/
return payload;
}
function warningInboundMiddleware(message) {
// check if message is null, perhaps someone forgot to resolve a promise
if (message == null) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: a middleware is returning an empty message'));
}
if (message.payload == null) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: a middleware is returning an empty payload in message'));
}
}
function prepareForConsole(payload) {
var result = _.clone(payload);
if (result.content instanceof Buffer) {
result.content = '<Buffer>';
}
if (result.ts != null) {
result.ts = result.ts.toString();
}
return result;
}
// eslint-disable-next-line max-params
function createMessage(chatId, userId, messageId, inboudMessage, chatServer) {
var options = chatServer.getOptions();
var contextProvider = options.contextProvider;
inboudMessage = inboudMessage || {};
return when(contextProvider.getOrCreate(chatId, {
chatId: chatId,
userId: userId,
messageId: messageId,
transport: options.transport,
authorized: false,
language: null
})
).then(function() {
return _.extend({}, inboudMessage, {
originalMessage: {
chatId: chatId,
userId: userId,
messageId: messageId,
transport: options.transport,
language: null
},
chat: function() {
return contextProvider.get(chatId);
},
api: function() {
return chatServer;
},
client: function() {
return options.connector;
}
});
});
}
function inboundMessage(payload, chatServer) {
var contextProvider = chatServer.getOptions().contextProvider;
if (chatServer.isDebug()) {
// eslint-disable-next-line no-console
console.log(orange('-- INBOUND MESSAGE --'));
// eslint-disable-next-line no-console
console.log(prettyjson.render(payload));
// eslint-disable-next-line no-console
console.log('');
}
// parse the message to extract the minimum payload needed for chat-platform to work properly
// could raise errors, relay to chat server
try {
var parsedMessage = parseMessage(payload, _this.options);
} catch(e) {
chatServer.emit('error', e);
return;
}
// create the node red message structure
var message = {
originalMessage: _.extend({}, payload, {
chatId: parsedMessage.chatId,
userId: parsedMessage.userId,
transport: parsedMessage.transport,
language: parsedMessage.language
}),
payload: {
type: parsedMessage.type,
chatId: parsedMessage.chatId,
userId: parsedMessage.userId,
ts: parsedMessage.ts,
transport: parsedMessage.transport,
inbound: true
},
chat: function() {
return contextProvider.get(parsedMessage.chatId);
},
api: function() {
return chatServer;
},
client: function() {
return chatServer.getOptions().connector;
}
};
// create empty promise
var stack = new Promise(function(resolve) {
resolve(message);
});
// if any context provider, then create the context
if (contextProvider != null) {
stack = stack
.then(function() {
return when(contextProvider.getOrCreate(parsedMessage.chatId, {
chatId: parsedMessage.chatId,
userId: parsedMessage.userId,
transport: parsedMessage.transport,
language: parsedMessage.language,
authorized: false
}));
})
.then(function() {
return when(message);
});
} else {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: context provider was not specified'));
}
// run general middleware
_(_this.uses).each(function(filter) {
stack = stack.then(function(message) {
warningInboundMiddleware(message);
return filter.call(chatServer, message);
});
});
// run ins middleware without any specific type
_(_this.ins).each(function(filter) {
stack = stack.then(function(message) {
warningInboundMiddleware(message);
// if message type is null
if (filter.type == null) {
return filter.method.call(chatServer, message);
}
return message;
});
});
// run ins middleware without a specific type
_(_this.ins).each(function(filter) {
stack = stack.then(function(message) {
warningInboundMiddleware(message);
// if message type is the same
if (filter.type === message.payload.type || filter.type === '*') {
return filter.method.call(chatServer, message);
}
return message;
});
});
// finally
stack
.then(function(message) {
if (message.payload != null && message.payload.type != null) {
if (chatServer.isDebug()) {
// eslint-disable-next-line no-console
console.log(orange('-- RELAY MESSAGE --'));
// eslint-disable-next-line no-console
console.log(prettyjson.render(prepareForConsole(message.payload)));
// eslint-disable-next-line no-console
console.log('');
}
chatServer.emit('message', message);
} else {
// do nothing
if (chatServer.isDebug()) {
// eslint-disable-next-line no-console
console.log(orange('-- DISCARDED MESSAGE (not handled by middlewares) --'));
// eslint-disable-next-line no-console
console.log('');
}
}
})
.catch(function(error) {
lcd.dump(error, 'Error in chat-platform.js');
if (chatServer != null) {
chatServer.emit('error', error);
}
});
}
function warningOutboundMiddleware(message) {
if (message == null) {
// eslint-disable-next-line no-console
console.log(yellow('WARNING: a middleware is returning an empty value'));
}
}
function outboundMessage(message, chatServer) {
// check if the message is from telegram
if (message.originalMessage != null && message.originalMessage.transport !== _this.options.transport) {
// exit, it's not from the current platform
if (chatServer.isDebug()) {
// eslint-disable-next-line no-console
console.log(yellow('Skipped incoming message for platform: ' + message.originalMessage.transport));
}
return;
}
if (chatServer.isDebug()) {
// eslint-disable-next-line no-console
console.log(orange('-- OUTBOUND MESSAGE --'));
// eslint-disable-next-line no-console
console.log(prettyjson.render(prepareForConsole(message.payload)));
// eslint-disable-next-line no-console
console.log('');
}
// create empty promise
var stack = new Promise(function(resolve) {
resolve(message);
});
// run general middleware
_(_this.uses).each(function(filter) {
stack = stack.then(function(message) {
warningOutboundMiddleware(message);
return filter.call(chatServer, message);
});
});
// run outs middleware without a specific typs
_(_this.outs).each(function(filter) {
stack = stack.then(function(message) {
// check if message is null, perhaps someone forgot to resolve a promise
warningOutboundMiddleware(message);
// if message type is the same
if (filter.type == null) {
return filter.method.call(chatServer, message);
}
return message;
});
});
// run outs middleware with a specific typs
_(_this.outs).each(function(filter) {
stack = stack.then(function(message) {
// check if message is null, perhaps someone forgot to resolve a promise
warningOutboundMiddleware(message);
// if message type is the same
if (message.payload != null && filter.type === message.payload.type) {
return filter.method.call(chatServer, message);
}
return message;
});
});
// finally
return stack
.catch(function(error) {
// eslint-disable-next-line no-console
console.log(red(error));
if (chatServer != null) {
chatServer.emit('error', error);
}
})
.then(function() {
// done
});
}
function unmountEvents(events, chatServer) {
var options = chatServer.getOptions();
var connector = options.connector;
if (connector != null) {
if (options.inboundMessageEvent != null) {
connector.off(options.inboundMessageEvent);
}
_(events).each(function (callback, eventName) {
connector.off(eventName);
});
}
}
function mountEvents(events, chatServer) {
var connector = chatServer.getOptions().connector;
if (connector != null) {
_(events).each(function (callback, eventName) {
connector.on(eventName, callback.bind(chatServer));
});
}
return when(true);
}
function unmountRoutes(RED, routes, chatServer) {
if (routes != null) {
var endpoints = _(routes).keys();
if (!_.isEmpty(endpoints)) {
var routesCount = RED.httpNode._router.stack.length;
var idx = 0;
var stack = RED.httpNode._router.stack;
for(; idx < stack.length;) {
var route = stack[idx];
if (route != null && route.name != null) {
var routeName = String(route.name).replace('bound ', '');
if (_.contains(endpoints, routeName)) {
stack.splice(idx, 1);
} else {
idx += 1;
}
} else {
idx += 1;
}
}
if (RED.httpNode._router.stack.length >= routesCount) {
// eslint-disable-next-line no-console
chatServer.error('improperly removed some routes, this will cause unexpected results and tricky bugs');
}
}
}
}
// eslint-disable-next-line max-params
function mountRoutes(RED, routes, routesDescription, chatServer) {
if (routes != null && RED == null) {
chatServer.warn('"RED" param is empty, impossible to mount the routes');
}
if (routes != null && RED != null) {
var uiPort = RED.settings.get('uiPort');
var options = chatServer.getOptions();
// eslint-disable-next-line no-console
console.log('');
// eslint-disable-next-line no-console
console.log(grey('------ WebHooks for ' + options.transport.toUpperCase() + '----------------'));
_(routes).map(function (middleware, route) {
RED.httpNode.use(route, middleware.bind(chatServer));
var description = null;
if (routesDescription != null && _.isString(routesDescription[route])) {
description = routesDescription[route];
} else if (routesDescription != null && _.isFunction(routesDescription[route])) {
description = routesDescription[route].call(chatServer);
}
// eslint-disable-next-line no-console
console.log(green('http://localhost' + (uiPort != '80' ? ':' + uiPort : '') + route)
+ (description != null ? grey(' - ') + white(description) : ''));
return null;
});
// eslint-disable-next-line no-console
console.log('');
}
return when(true);
}
var methods = {
'in': function() {
var type = null;
var method = null;
if (arguments.length === 1) {
method = arguments[0];
} else if (arguments.length === 2) {
type = arguments[0];
method = arguments[1];
} else {
throw '.in() wrong number of parameters';
}
_this.ins.push({
type: type,
method: method
});
return methods;
},
out: function() {
var type = null;
var method = null;
if (arguments.length === 1) {
method = arguments[0];
} else if (arguments.length === 2) {
type = arguments[0];
method = arguments[1];
} else {
throw '.out() wrong number of parameters';
}
_this.outs.push({
type: type,
method: method
});
return methods;
},
use: function(method) {
_this.uses.push(method);
return methods;
},
mixin: function(obj) {
_this._mixins = _.extend(_this._mixin || {}, obj);
return methods;
},
createServer: function(options) {
options = _.extend({}, _this.options, options);
var chatServer = null;
var ChatServer = function(options) {
this.options = options;
this.warn = function(msg) {
var text = '[' + options.transport.toUpperCase() + '] ' + msg;
// eslint-disable-next-line no-console
console.log(yellow(text));
this.emit('warning', text);
};
this.error = function(msg) {
var text = '[' + options.transport.toUpperCase() + '] ' + msg;
// eslint-disable-next-line no-console
console.log(red(text));
this.emit('error', text);
};
this.getOptions = function() {
return this.options;
};
this.isDebug = function() {
return this.options != null && this.options.debug;
};
this.getConnector = function() {
return this.options.connector;
};
this.send = function(message) {
return outboundMessage(message, this);
};
this.receive = function(message) {
inboundMessage(message, this);
};
// eslint-disable-next-line max-params
this.createMessage = function(chatId, userId, messageId, inboudMessage) {
return createMessage(chatId, userId, messageId, inboudMessage, this);
};
this.start = function() {
var _this = this;
var stack = when(true);
var options = this.getOptions();
if (_.isFunction(options.onStart)) {
// execute on start callback, ensure it's a properly chained promise
stack = stack.then(function() {
return when(options.onStart.call(chatServer));
});
}
return stack
.then(function() {
return mountRoutes(options.RED, options.routes, options.routesDescription, _this);
})
.then(function() {
return mountEvents(options.events, _this);
})
.then(function() {
return _.isFunction(options.onStarted) ? when(options.onStarted.call(_this)) : when(true);
})
.then(function() {
if (_this.isDebug()) {
// eslint-disable-next-line no-console
console.log(green('Chat server started, transport: ') + white(options.transport));
}
// listen to inbound event
var connector = options.connector;
if (connector != null) {
connector.on(options.inboundMessageEvent, function (message) {
inboundMessage(message, chatServer);
});
}
_this.emit('start');
},
function(error) {
// eslint-disable-next-line no-console
_this.error(error);
});
};
this.stop = function() {
this.emit('stop');
var options = this.getOptions();
unmountRoutes(options.RED, options.routes, this);
unmountEvents(options.events, this);
var stack = when(true);
if (_.isFunction(options.onStop)) {
stack = stack.then(function() {
return when(options.onStop.call(chatServer));
});
}
return stack;
};
EventEmitter.call(this);
};
inherits(ChatServer, EventEmitter);
_.extend(ChatServer.prototype, _this._mixins);
// create chat instance
chatServer = new ChatServer(options);
return chatServer;
}
};
return methods;
};
module.exports = ChatExpress;