UNPKG

@onboardmobility/whatsapp

Version:

Utility class to connect to WhatsApp APIs

404 lines (348 loc) 12.2 kB
'use strict'; /** * 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; } } };