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
231 lines (208 loc) • 7.6 kB
JavaScript
const _ = require('underscore');
const utils = require('./helpers/utils');
const when = utils.when;
const moment = require('moment');
const isEmpty = value => _.isEmpty(value) && !_.isNumber(value);
const byString = function(o, s) {
let str = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
str = str.replace(/^\./, ''); // strip a leading dot
var a = str.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}
module.exports = (msg, node, opts) => {
// these values are not stored in chat context, a user accessing the chatbot with different
// chatIds could have the same chat context, the single source of truth for these values is
// the originalMessage
const specials = {
chatId: msg.originalMessage?.chatId,
userId: msg.originalMessage?.userId,
messageId: msg.originalMessage?.messageId, // for retrocompatibility
inboundMessageId: msg.originalMessage?.messageId,
transport: msg.originalMessage?.transport
};
// set current message in specials
if (msg.payload != null && _.isString(msg.payload.content)) {
specials.message = msg.payload.content;
} else if (msg.previous != null && _.isString(msg.previous.content)) {
specials.message = msg.previous.content;
}
const payload = _.isObject(msg.payload) ? msg.payload : {};
const options = Object.assign({
preserveNumbers: false // do not translate numbers into string
}, opts)
// extract subtokens from a object value
const extractObjectKeys = (value, subtokens) => {
let result = value;
let currentValue = value;
if (_.isArray(subtokens) && !_.isEmpty(subtokens)) {
_(subtokens).each(function(subtoken) {
if (_.isObject(currentValue) && currentValue[subtoken] != null) {
currentValue = currentValue[subtoken];
}
});
result = currentValue;
}
return result;
};
const getTokenValue = async (token, language) => {
let value = null;
const context = node.context();
const subtokens = token.split('.');
const variable = subtokens[0];
const chatContext = msg.chat();
if (token === 'payload') {
value = String(msg.payload);
} else if (token.startsWith('payload.')) {
const stringPath = token.replace('payload.', '');
value = byString(payload, stringPath);
value = value != null ? value : token;
} else if (token.startsWith('msg.')) {
const stringPath = token.replace('msg.', '');
value = byString(msg, stringPath);
value = value != null ? value : token;
} else if (token.startsWith('global.')) {
// avoid undefined
value = context.global.get(subtokens[1]) != null ? context.global.get(subtokens[1]) : null
} else if (token.startsWith('tx.')) {
// use global translator
const tx = context.global.get('tx');
value = _.isFunction(tx) ? tx(token.replace('tx.', ''), language) : token;
} else if (specials[token] != null) {
value = specials[token];
} else {
// excluded global, payload, need to access to the context
let fromContext = await when(chatContext.get(variable));
if (fromContext != null) {
value = fromContext;
if (subtokens.length > 0) {
value = extractObjectKeys(value, subtokens.slice(1));
}
} else if (!isEmpty(context.flow.get(variable))) {
value = context.flow.get(variable);
} else if (!isEmpty(context.get(variable))) {
value = context.get(variable);
} else if (!isEmpty(context.global.get(variable))) {
value = context.global.get(variable);
} else if (!isEmpty(msg[variable])) {
value = msg[variable];
}
}
return value;
};
const replaceTokens = (message, tokens, language) => {
let stack = new Promise(resolve => resolve(message));
if (tokens != null && tokens.length !== 0) {
// replace all tokens
_(tokens).each(token => {
stack = stack.then(message => {
return new Promise(resolve => {
getTokenValue(token, language)
.then(value => {
// if value was not fount, leve the token in place
if (value != null) {
// todo take the format date from config
const formattedValue = value instanceof moment ? value.format('DD MMMM,YYYY') : value;
resolve(message.replace(new RegExp(`{{${token}}}`, 'g'), formattedValue));
} else {
resolve(message);
}
});
});
});
});
}
return stack;
};
const renderString = (message, language) => {
if (_.isEmpty(message)) {
return message;
}
// extract tokens
let tokens = message.match(/\{\{([A-Za-z0-9\-\._аАбБвВгГдДеЕёЁжЖзЗиИйЙкКлЛмМнНоОпПрРсСтТуУфФхХцЦчЧшШщЩъЪыЫьЬэЭюЮяЯ]*?)\}\}/g);
if (tokens != null && tokens.length != 0) {
tokens = tokens.map(token => token.replace('{{', '').replace('}}', ''));
}
// replace them
return replaceTokens(message, tokens, language);
}
const renderObject = (item, language) => {
let task = when(true);
const result = {};
return new Promise(resolve => {
_(item).each((value, key) => {
task = task
.then(() => renderItem(value, language))
.then(translated => result[key] = translated);
});
task.then(() => resolve(result));
});
};
const renderArray = (item, language) => {
let task = when(true);
const result = [];
return new Promise(resolve => {
_(item).each(value => {
task = task
.then(() => renderItem(value, language))
.then(translated => result.push(translated));
});
task.then(() => resolve(result));
});
}
const renderItem = (item, language) => {
if (_.isString(item)) {
return renderString(item, language);
} else if (!options.preserveNumbers && _.isNumber(item)) {
return renderString(String(item), language);
} else if (options.preserveNumbers && _.isNumber(item)) {
return item;
} else if (_.isArray(item)) {
return renderArray(item, language);
} else if (item instanceof Buffer) {
return when(item); // leave buffers untouched
} else if (_.isObject(item)) {
return renderObject(item, language);
}
return when(item);
}
const template = function() {
const toTranslate = Array.prototype.slice.call(arguments, 0);
const chatContext = msg.chat();
return new Promise(resolve => {
const translated = [];
let language;
let task = when(chatContext.get('language'))
.then(lang => language = !_.isEmpty(lang) ? lang : 'en');
// translate each element of the array
_(toTranslate).each(sentence => {
task = task
.then(() => renderItem(sentence, language))
.then(message => translated.push(message));
});
// finally
task.then(() => {
if (translated.length === 1) {
resolve(translated[0]);
} else {
resolve(translated);
}
});
});
};
// evaluate a single string
template.evaluate = async function(token) {
const chatContext = msg.chat();
const language = await when(chatContext.get('language'));
const result = await renderString(token, language);
return result;
};
return template;
};