wingbot
Version:
Enterprise Messaging Bot Conversation Engine
340 lines (297 loc) • 8.97 kB
JavaScript
/*
* @author David Menger
*/
;
const hbs = require('./hbs');
const stateData = require('../utils/stateData');
const getCondition = require('../utils/getCondition');
const ASPECT_SQUARE = 'square';
const ASPECT_HORISONTAL = 'horisontal';
const TYPE_SHARE = 'element_share';
const TYPE_URL = 'web_url';
const TYPE_URL_WITH_EXT = 'web_url_extension';
const TYPE_POSTBACK = 'postback';
const TYPE_ATTACHMENT = 'attachment';
const WEBVIEW_FULL = 'full';
const WEBVIEW_TALL = 'tall';
const WEBVIEW_COMPACT = 'compact';
/**
* @typedef {Translation[] | Translation | string[] | string} Translations
*/
/**
* @typedef {object} TextObject
* @prop {string} t - text
* @prop {string} l - lang
* @prop {string|null} p - purpose
*/
/**
* null = text+voice
* t = text only
* v = voice only
* s = ssml
*
* @typedef {"t"|"v"|"s"|null} Purpose
*/
/**
* @typedef Translation
* @property {string|string[]} t - text alternatives
* @property {string | null} l - language
* @property {Purpose | Purpose[]} [p] - purposes
*/
/**
* @param {string | string[] | Translation | Translation[]} translations
* @returns {translations is Translation[]}
*/
function isArrayOfObjects (translations) {
return Array.isArray(translations)
&& typeof translations[0] === 'object'
&& translations[0] !== null;
}
function isTextObjectEmpty (text) {
if (!text || !text.t) {
return true;
}
if (Array.isArray(text.t)) {
return !text.t.some((t) => !!t);
}
return false;
}
/**
* @param {Translations} translations
* @param {string} [lang]
* @param {boolean} [disableDefaulting] - it will try to find translation for other language
* @returns {null|string|string[]}
*/
function getLanguageText (translations, lang = null, disableDefaulting = false) {
let foundText;
if (isArrayOfObjects(translations)) {
if (lang) {
// @ts-ignore
foundText = translations.find((t) => t.l === lang);
} else if (disableDefaulting) {
// @ts-ignore
[foundText] = translations;
}
if (isTextObjectEmpty(foundText) && !disableDefaulting) {
// @ts-ignore
foundText = translations.find((t) => !isTextObjectEmpty(t));
}
foundText = foundText ? foundText.t : null;
} else {
foundText = translations;
}
if (Array.isArray(foundText)) {
foundText = foundText.filter((f) => !!f);
if (foundText.length === 0) {
return '';
}
}
return foundText || '';
}
/**
*
* @param {Translations} translations
* @param {string} [lang]
* @returns {TextObject[]}
*/
function getLanguageTextObjects (translations, lang = null) {
/** @type {{t:string|string[],l?:string,p?:string|string[]}[]} */
let foundTexts;
if (!Array.isArray(translations) || typeof translations[0] === 'string') {
// @ts-ignore
foundTexts = [{ t: translations, l: lang }];
} else { // is array of objects
foundTexts = translations
// @ts-ignore
.filter(({ l = null, ...to }) => l === lang && !isTextObjectEmpty(to));
if (foundTexts.length === 0) {
const { l: firstNonEmptyLang = null } = translations
// @ts-ignore
.find((to) => !isTextObjectEmpty(to)) || {};
foundTexts = translations
// @ts-ignore
.filter(({ l = null, ...to }) => l === firstNonEmptyLang && !isTextObjectEmpty(to));
}
}
return foundTexts
.reduce((a, to) => {
if (Array.isArray(to.t)) {
const purposes = Array.isArray(to.p)
? to.p
: [to.p];
a.push(
...to.t.map((t, i) => ({
...to, t, p: purposes[i] || null
}))
);
} else {
a.push(to);
}
return a;
}, [])
.filter((to) => !!to.t)
.map(({
t, l = null, p = null, ...rest
}) => ({
t, l, p, ...rest
}));
}
function renderMessageText (fn, data) {
const renderer = fn === 'function' ? fn : hbs.compile(fn);
return renderer(data).trim();
}
function randomizedCompiler (text, lang) {
const texts = getLanguageText(text, lang);
if (!Array.isArray(texts)) {
return hbs.compile(texts);
}
if (texts.length === 1) {
return hbs.compile(texts[0]);
}
return (...args) => {
const [data = {}] = args;
if (data._expandRandomTexts) {
return texts
.map((t, i) => {
let compiled;
if (typeof t !== 'function') {
compiled = hbs.compile(t);
texts[i] = compiled;
} else {
compiled = t;
}
return compiled(...args);
})
.join(' ');
}
const index = Math.floor(Math.random() * texts.length);
if (typeof texts[index] !== 'function') {
texts[index] = hbs.compile(texts[index]);
}
return texts[index](...args);
};
}
/**
* l - language
* t - alternatives
*
* @param {{l:string,t:string[]}[] | string} text
* @returns {(state:any)=>string}
*/
function cachedTranslatedCompilator (text) {
const cache = new Map();
return (state) => {
const { lang: key = '-', lang } = state;
let renderer = cache.get(key);
if (!renderer) {
renderer = randomizedCompiler(text, lang);
cache.set(key, renderer);
}
return renderer(state);
};
}
function getText (text, state) {
const renderer = randomizedCompiler(text, state.lang);
return renderer(state);
}
// eslint-disable-next-line no-unused-vars
const DEFAULT_LINK_TRANSLATOR = (senderId, text, url, isExtUrl, state, pageId) => url;
/** @typedef {import('../BuildRouter').BotContext} BotContext */
function processButtons (
buttons,
state,
elem,
senderId,
context,
req,
res
) {
const translateLinks = context.linksTranslator || DEFAULT_LINK_TRANSLATOR;
buttons.forEach(({
title: btnTitle,
action: btnAction,
hasCondition,
conditionFn,
hasEditableCondition,
editableCondition,
setState
}) => {
if (hasCondition) {
const condition = getCondition({
hasCondition, conditionFn, hasEditableCondition, editableCondition
}, context, 'Button condition');
if (!condition(req, res)) {
return;
}
}
const btnTitleText = getText(btnTitle, state);
const defaultText = getText(btnTitle, { lang: null });
const {
type,
url,
webviewHeight = WEBVIEW_TALL,
targetRouteId,
action,
payload
} = btnAction;
const isExtUrl = type === TYPE_URL_WITH_EXT;
switch (type) {
case TYPE_URL:
case TYPE_URL_WITH_EXT: {
const hasExtention = type === TYPE_URL_WITH_EXT;
let urlText = getText(url, state);
urlText = translateLinks(
senderId,
defaultText,
urlText,
isExtUrl,
state,
req.pageId
);
elem.urlButton(btnTitleText, urlText, hasExtention, webviewHeight);
break;
}
case TYPE_POSTBACK: {
let postbackAction = context.linksMap.get(targetRouteId) || action;
if (postbackAction === '/') {
postbackAction = './';
} else if (!postbackAction) {
return;
}
elem.postBackButton(btnTitleText, postbackAction, {}, setState);
break;
}
case TYPE_SHARE:
elem.shareButton(btnTitleText);
break;
case TYPE_ATTACHMENT: {
const translatedPayloadContent = getText(payload.content, state);
elem.attachmentButton(btnTitleText, {
...payload,
content: translatedPayloadContent
});
break;
}
default:
}
});
}
module.exports = {
getLanguageText,
getLanguageTextObjects,
cachedTranslatedCompilator,
randomizedCompiler,
getText,
stateData,
renderMessageText,
ASPECT_SQUARE,
ASPECT_HORISONTAL,
TYPE_SHARE,
TYPE_URL,
TYPE_URL_WITH_EXT,
TYPE_POSTBACK,
WEBVIEW_FULL,
WEBVIEW_TALL,
WEBVIEW_COMPACT,
processButtons
};