@onboardmobility/whatsapp
Version:
Utility class to connect to WhatsApp APIs
404 lines (348 loc) • 12.2 kB
JavaScript
;
/**
* 3rd party
*/
const request = require('request-promise');
const fs = require('fs');
const blobUtil = require('@onboardmobility/azure');
const chatApi = require('./chatApi');
const maytApi = require('./maytApi');
const wassengerApi = require('./wassengerApi');
const watiApi = require('./watiApi');
const wpApi = require('./wpApi');
let PHRASES = {};
if (process.env.LANG_FILE) {
PHRASES = JSON.parse(fs.readFileSync(process.env.LANG_FILE));
}
/**
* Envs vars
*/
const { WA_TYPE,
QNA_ID, QNA_KEY, QNA_URL,
LUIS_ID, LUIS_KEY, LUIS_URL, CONTAINER_NAME,
RASA_URL, HAS_RASA } = process.env;
/**
* Class for WhatsApp Service
*/
module.exports = class WhatsAppService {
/**
* Create the WhatsAppService
*/
constructor() {
}
/**
* Return the option from the title
* @param {Integer} title the title
*/
async getOptionFromTitle(title) {
const INITIAL_OPTIONS = await this.getInitialOptions();
const options = Object.values(INITIAL_OPTIONS);
const option = options.find(function (item) {
return item.title === title;
});
return option;
}
/**
* Handle the user intent.
* @param {String} text the user text
*/
async getIntent({ text }) {
try {
if (HAS_RASA) {
return await this.getRasaIntent({ text });
}
const resp = JSON.parse(await request.get({
url: `${LUIS_URL}${LUIS_ID}?verbose=true&timezoneOffset=-360&subscription-key=${LUIS_KEY}&q=${encodeURIComponent(text)}`,
}));
const topScoringIntent = resp.topScoringIntent;
if (topScoringIntent && topScoringIntent.score > 0.75) {
return topScoringIntent.intent;
}
return 'None';
} catch (e) {
return 'None';
}
}
/**
* Handle the user intent using RASA
* @param {String} text the user text
*/
async getRasaIntent({ text }) {
try {
let resp = JSON.parse(await request.post({
url: `${RASA_URL}`,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ 'text': text }),
}));
if (resp && resp.prediction) {
return resp.prediction.topIntent;
}
return 'None';
} catch (e) {
return 'None';
}
}
/**
* Handle the user question
* @param {String} text the user text
*/
async getQA({ text }) {
let answer = 'Não foi possível encontrar uma resposta para essa pergunta';
try {
let resp = await request.post({
url: `${QNA_URL}${QNA_ID}/generateAnswer`,
headers: {
'Authorization': `EndpointKey ${QNA_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ 'question': text }),
});
resp = resp.toString().replace(/\\\"\\\"/g, '\"');
resp = resp.replace(/\"\\\"/g, '');
resp = resp.replace(/\\\"\"/g, '');
resp = JSON.parse(resp);
answer = resp.answers.find(function (an) {
return an.score > 40;
});
if (!answer) {
return 'Não foi possível encontrar uma resposta para essa pergunta';
} else {
return answer;
}
} catch (e) {
return answer;
}
}
/**
* Update on storage the last user interaction
* @param {Object} object the params containing author (must have) and any other elements
*/
async updatePreviousIntent(object) {
if (CONTAINER_NAME && object && object.author) {
await blobUtil.saveBlob(CONTAINER_NAME, object.author, JSON.stringify(object));
}
}
/**
* Get on storage the last user interaction
* @param {String} author the author
*/
async handlePreviousIntent({ author }) {
try {
const resp = await blobUtil.getBlob(CONTAINER_NAME, author);
return JSON.parse(resp.text);
} catch (e) {
return {};
}
}
/**
* Handle the intent processing the text itself
* @param {Object} the param with the author, senderName and body
*/
async handleIntentByLuis({ body, previousIntent, author, senderName }) {
const intent = await this.getIntent({ text: body });
if (!intent) {
return false;
}
const INITIAL_OPTIONS = await this.getInitialOptions();
const keys = Object.keys(INITIAL_OPTIONS);
const keySelected = keys.find(function (key) {
const opt = INITIAL_OPTIONS[key];
return opt.intent.includes(intent);
});
if (keySelected) {
const option = INITIAL_OPTIONS[keySelected];
if (option) {
previousIntent.title = '';
await option.handler({ previousIntent, author, senderName, body });
return true;
}
}
return false;
}
/**
* Handle if the intent using the user typed option and previous intent
* @param {Object} the param with the previous intent, author, senderName and body
*/
async handleIntentByTitle({ previousIntent, author, senderName, body }) {
let title = '';
let hasPrevious = false;
if (previousIntent && previousIntent.title) {
hasPrevious = true;
title = previousIntent.title;
} else {
const INITIAL_OPTIONS = await this.getInitialOptions();
if (isNaN(body)) {
title = '';
} else {
let aux = parseInt(body);
for (const option of Object.values(INITIAL_OPTIONS)) {
if (option.show) {
aux--;
}
if (aux === 0) {
title = option.title;
break;
}
}
}
}
const option = await this.getOptionFromTitle(title);
if (option) {
option.handler({ previousIntent, author, senderName, body });
} else {
await this.handleWelcome({ previousIntent, author, senderName, body });
}
}
/**
* Get the text referenced by the label
* @param {String} label the label
*/
getText(label) {
try {
let parts = label.split('.');
let object;
for (const part of parts) {
if (object) {
object = object[part];
} else {
object = PHRASES[part];
}
}
return object;
} catch (e) {
return '';
}
}
/**
* Return the incoming message info
* @param {Object} data the webhook payload
*/
async getIncomingMessageInfo(data) {
const impl = await this.getImplementation();
const incomingData = await impl.processIncomingMessages(data);
return incomingData;
}
/**
* Process the incoming messages
* @param {Object} data the webhook payload
*/
async processIncomingMessages(data) {
const impl = await this.getImplementation();
const incomingData = await impl.processIncomingMessages(data);
const { fromMe, type, author, body, result, senderName } = incomingData;
if (!fromMe && result) {
// Handle invalid type of messages
if (type !== 'chat' && type !== 'text' && type !== 'message' && type !== 'button_reply') {
await this.sendMessage({ to: author, text: 'Digite a opção desejada ou 0 para menu inicial' });
}
// Check the intent based on the previous one and the new text
const previousIntent = await this.handlePreviousIntent({ author });
// Do not process any message when loading
if (previousIntent.isLoading) {
await this.sendMessage({
to: author,
text: previousIntent.loadingText
? previousIntent.loadingText : 'Por favor, aguarde...',
});
}
// Set senderName
const finalSenderName = previousIntent && previousIntent.senderName ?
previousIntent.senderName : senderName ? senderName : author;
const welcomeOptions = await this.getWelcomeOptions();
// Always handle 0 to initial
if (welcomeOptions.includes(body.toUpperCase())) {
return await this.handleWelcome({ previousIntent, author, senderName: finalSenderName, body });
}
try {
if (!isNaN(body)) {
await this.handleIntentByTitle({ body, previousIntent, author, senderName: finalSenderName });
} else {
let handled = false;
if (previousIntent && !previousIntent.blockLuis) {
handled = await this.handleIntentByLuis({ body, previousIntent, author, senderName: finalSenderName })
}
if (!handled) {
await this.handleIntentByTitle({ body, previousIntent, author, senderName: finalSenderName });
}
}
} catch (e) {
if (!previousIntent || (previousIntent && !previousIntent.blockLuis)) {
await this.handleIntentByLuis({ body, previousIntent, author, senderName: finalSenderName });
}
}
}
}
/**
* Return the initial options that should trigger the welcome intent
* @returns an array with the options
*/
async getWelcomeOptions() {
return ['0'];
}
/**
* Send the message using one of the providers
* @param {String} caption the message caption
* @param {String} text the message text
* @param {String} url the message url
* @param {String} to the receving phone
*/
async sendMessage({ caption, text, url, to }) {
const impl = await this.getImplementation();
await impl.sendMessage({ caption, text, url, to });
return true;
}
/**
* Send files
* @param {Object} params the data needed:
* to: destination
* text: URL or data
* filename: attachment name
* caption: optional caption
*/
async sendFile({ to, url, filename, caption }) {
const impl = await this.getImplementation();
await impl.sendFile({ caption, url, filename, to });
return true;
}
/**
* Send a list or button message to the user
* @param {Object} params the data needed:
* to: destination
* header: optional header text
* body: the message body
* footer: option footer text
* actions: the list of actions
* type: button or list
*/
async sendInteractive({ to, header, body, footer, actions, type, previousIntent }) {
if (!previousIntent) {
previousIntent = await this.handlePreviousIntent({ author: to });
}
previousIntent.actions = actions;
await this.updatePreviousIntent(previousIntent);
const impl = await this.getImplementation();
await impl.sendInteractive({ to, header, body, footer, actions, type });
return true;
}
/**
* Return the implementation based on the WA_TYPE
*/
async getImplementation() {
switch (WA_TYPE) {
case '0':
return chatApi;
case '1':
return wassengerApi;
case '2':
return maytApi;
case '3':
return watiApi;
case '4':
return wpApi;
default:
return chatApi;
}
}
};