From 6f62453f4e1f813e4686bece6419f42b6b73bf18 Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Mon, 6 Mar 2023 20:36:27 -0300 Subject: [PATCH] fix(whatsapp.gblib): #288 fix list and buttons. --- package.json | 4 +- .../basic.gblib/services/DialogKeywords.ts | 136 ++++++++------- .../services/GBConversationalService.ts | 7 +- .../services/WhatsappDirectLine.ts | 157 +++++++++--------- 4 files changed, 149 insertions(+), 155 deletions(-) diff --git a/package.json b/package.json index b9d2aec9..8f372d0c 100644 --- a/package.json +++ b/package.json @@ -172,10 +172,10 @@ "vm2-process": "2.1.1", "walk-promise": "0.2.0", "washyourmouthoutwithsoap": "1.0.2", - "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js#fix-buttons-list", + "whatsapp-web.js": "github:meetscrm/whatsapp-web.js#8a46b65e2284ae2b031b5a9217e33ec2bab2e579", "winston": "3.8.2", "winston-logs-display": "1.0.0", - "ws": "^8.12.1", + "ws": "8.12.1", "yarn": "1.22.19" }, "devDependencies": { diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index 79031051..1dd9456d 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -46,20 +46,19 @@ import { Messages } from '../strings.js'; import * as Fs from 'fs'; import { CollectionUtil } from 'pragmatismo-io-framework'; import { GBConversationalService } from '../../core.gbapp/services/GBConversationalService.js'; -import { GuaribasUser } from '../../security.gbapp/models/index.js'; import phoneUtil from 'google-libphonenumber'; import phone from 'phone'; import DateDiff from 'date-diff'; -import { Buttons, List } from 'whatsapp-web.js'; import tesseract from 'node-tesseract-ocr'; import Path from 'path'; import sgMail from '@sendgrid/mail'; import mammoth from 'mammoth'; import qrcode from 'qrcode'; -import { json } from 'body-parser'; import { WebAutomationServices } from './WebAutomationServices.js'; import urljoin from 'url-join'; import QrScanner from 'qr-scanner'; +import pkg from 'whatsapp-web.js'; +const { List, Buttons } = pkg; /** * Default check interval for user replay @@ -70,7 +69,6 @@ const DEFAULT_HEAR_POLL_INTERVAL = 500; * Base services of conversation to be called by BASIC. */ export class DialogKeywords { - /** * * Data = [10,20,30] @@ -84,7 +82,7 @@ export class DialogKeywords { * @param legends * @see https://www.npmjs.com/package/plot */ - public async chart({pid, type, data, legends, transpose }) { + public async chart({ pid, type, data, legends, transpose }) { const { min, user } = await DialogKeywords.getProcessInfo(pid); let table = [[]]; @@ -169,7 +167,7 @@ export class DialogKeywords { * * @example x = TODAY */ - public async getToday({pid}) { + public async getToday({ pid }) { const { min, user } = await DialogKeywords.getProcessInfo(pid); let d = new Date(), month = '' + (d.getMonth() + 1), @@ -206,28 +204,28 @@ export class DialogKeywords { * * @example EXIT */ - public async exit({ }) { } + public async exit({}) {} /** * Get active tasks. * * @example list = ACTIVE TASKS */ - public async getActiveTasks({ pid }) { } + public async getActiveTasks({ pid }) {} /** * Creates a new deal. * * @example CREATE DEAL dealname,contato,empresa,amount */ - public async createDeal({ pid, dealName, contact, company, amount }) { } + public async createDeal({ pid, dealName, contact, company, amount }) {} /** * Finds contacts in XRM. * * @example list = FIND CONTACT "Sandra" */ - public async fndContact({ pid, name }) { } + public async fndContact({ pid, name }) {} public getContentLocaleWithCulture(contentLocale) { switch (contentLocale) { @@ -258,7 +256,7 @@ export class DialogKeywords { * @example day = WEEKDAY (date) * */ - public async getWeekFromDate({pid, date}) { + public async getWeekFromDate({ pid, date }) { const { min, user } = await DialogKeywords.getProcessInfo(pid); const contentLocale = min.core.getParam( min.instance, @@ -431,7 +429,7 @@ export class DialogKeywords { * @example SAVE "contacts.xlsx", name, email, NOW * */ - public async getNow({pid}) { + public async getNow({ pid }) { const { min, user } = await DialogKeywords.getProcessInfo(pid); const contentLocale = min.core.getParam( min.instance, @@ -623,16 +621,16 @@ export class DialogKeywords { await this.setOption({ pid, name: 'wholeWord', value: value }); } -/** + /** * Defines the FIND behaviour to consider whole words while searching. * * @example SET FILTER TYPE date, string * */ -public async setFilterTypes({ pid, types }) { - const value = types; - await this.setOption({ pid, name: 'filterTypes', value: value }); -} + public async setFilterTypes({ pid, types }) { + const value = types; + await this.setOption({ pid, name: 'filterTypes', value: value }); + } /** * Defines the theme for assets generation. @@ -678,7 +676,7 @@ public async setFilterTypes({ pid, types }) { * @example MENU * */ - public async showMenu({ }) { + public async showMenu({}) { // https://github.com/GeneralBots/BotServer/issues/237 // return await beginDialog('/menu'); } @@ -704,15 +702,16 @@ public async setFilterTypes({ pid, types }) { * @example HEAR name * */ - public async hear({ pid, kind, arg }) { + public async hear({ pid, kind, args }) { let { min, user, params } = await DialogKeywords.getProcessInfo(pid); // Handles first arg as an array of args. - let args = []; - if (arg && arg.length) { - args = arg; + let args1 = []; + if (args && args.length) { + args1 = args; } + args = args1; try { const isIntentYes = (locale, utterance) => { @@ -736,28 +735,25 @@ public async setFilterTypes({ pid, types }) { // https://github.com/GeneralBots/BotServer/issues/266 if (args && args.length > 1) { - // https://github.com/pedroslopez/whatsapp-web.js/issues/1811 - // - // const list = new List( - // 'Escolha um dos itens', - // 'Itens1', - // [ - // { - // title: 'Itens2', - // rows: [] - // } - // ], - // 'Please select a product' - // ); - // let i = 0; - // await CollectionUtil.asyncForEach(args, async arg => { - // i++; - // list.sections[0].rows.push({ title: arg, id: `button${i}` }); - // await this.getTalk(arg); - // }); + let i = 0; - // const button = new wpp.Buttons(Messages[locale].choices, choices, ' ', ' '); - // await this.getTalk(button); + if (args.length > 3) { + let section = { title: '', rows: [] }; + await CollectionUtil.asyncForEach(args, async arg => { + i++; + section.rows.push({ title: arg, id: `button${i}` }); + }); + const list = new List('Select:', '', [section], '', ''); + await this.talk({ pid: pid, text: list }); + } else { + let buttons = []; + await CollectionUtil.asyncForEach(args, async arg => { + i++; + buttons.push({ body: arg, id: `button${i}` }); + }); + let button = new Buttons('Select:', buttons, '', 'General Bots'); + await this.talk({ pid: pid, text: button }); + } GBLog.info(`BASIC: HEAR with [${args.toString()}] (Asking for input).`); } else { @@ -781,7 +777,6 @@ public async setFilterTypes({ pid, types }) { const answer = min.cbMap[userId].promise; if (kind === 'sheet') { - // Retrieves the .xlsx file associated with the HEAR var AS file.xlsx. let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min); @@ -795,10 +790,10 @@ public async setFilterTypes({ pid, types }) { // Finds .xlsx specified by arg. const document = res.value.filter(m => { - return m.name === arg; + return m.name === args; }); if (document === undefined || document.length === 0) { - GBLog.info(`${arg} not found on .gbdata folder, check the package.`); + GBLog.info(`${args} not found on .gbdata folder, check the package.`); return null; } @@ -818,8 +813,7 @@ public async setFilterTypes({ pid, types }) { for (; index < results.text.length; index++) { if (results.text[index][0] !== '') { list.push(results.text[index][0]); - } - else { + } else { break; } } @@ -837,9 +831,8 @@ public async setFilterTypes({ pid, types }) { if (result === null) { await this.talk({ pid, text: `Escolha por favor um dos itens sugeridos.` }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } - } else if (kind === 'file') { GBLog.info(`BASIC (${min.botId}): Upload done for ${answer.filename}.`); const handle = WebAutomationServices.cyrb53(min.botId + answer.filename); @@ -860,7 +853,7 @@ public async setFilterTypes({ pid, types }) { if (value === null) { await this.talk({ pid, text: 'Por favor, digite um e-mail válido.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -873,7 +866,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um nome válido.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -886,7 +879,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um número válido.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -901,7 +894,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite uma data no formato 12/12/2020.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -914,7 +907,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um horário no formato hh:ss.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -933,7 +926,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um valor monetário.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value; @@ -946,21 +939,22 @@ public async setFilterTypes({ pid, types }) { } catch (error) { await this.talk({ pid, text: Messages[locale].validation_enter_valid_mobile }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } if (!phoneUtil.isPossibleNumber(phoneNumber)) { await this.talk({ pid, text: 'Por favor, digite um número de telefone válido.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = phoneNumber; - } else if (kind === 'qr-scanner'){ + } else if (kind === 'qr-scanner') { //https://github.com/GeneralBots/BotServer/issues/171 GBLog.info(`BASIC (${min.botId}): Upload done for ${answer.filename}.`); const handle = WebAutomationServices.cyrb53(min.botId + answer.filename); GBServer.globals.files[handle] = answer; - QrScanner.scanImage(GBServer.globals.files[handle]).then(result => console.log(result)).catch(error => console.log(error || 'no QR code found.')); - + QrScanner.scanImage(GBServer.globals.files[handle]) + .then(result => console.log(result)) + .catch(error => console.log(error || 'no QR code found.')); } else if (kind === 'zipcode') { const extractEntity = (text: string) => { text = text.replace(/\-/gi, ''); @@ -977,7 +971,7 @@ public async setFilterTypes({ pid, types }) { if (value === null || value.length != 1) { await this.talk({ pid, text: 'Por favor, digite um CEP válido.' }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } result = value[0]; @@ -992,7 +986,7 @@ public async setFilterTypes({ pid, types }) { if (result === null) { await this.talk({ pid, text: `Escolha por favor um dos itens sugeridos.` }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } } else if (kind === 'language') { result = null; @@ -1024,7 +1018,7 @@ public async setFilterTypes({ pid, types }) { if (result === null) { await this.talk({ pid, text: `Escolha por favor um dos itens sugeridos.` }); - return await this.hear({ pid, kind, arg }); + return await this.hear({ pid, kind, args }); } } return result; @@ -1146,12 +1140,12 @@ public async setFilterTypes({ pid, types }) { await min.conversationalService.sendFile(min, null, mobile, url, caption); } } -/** - * Generates a new QRCode. - * - * file = QRCODE "data" - * - */ + /** + * Generates a new QRCode. + * + * file = QRCODE "data" + * + */ public async getQRCode({ pid, text }) { const { min, user } = await DialogKeywords.getProcessInfo(pid); const img = await qrcode.toDataURL(text); @@ -1163,6 +1157,6 @@ public async setFilterTypes({ pid, types }) { Fs.writeFileSync(localName, buf, { encoding: null }); const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName)); - return {data: data, localName: localName, url: url}; + return { data: data, localName: localName, url: url }; } } diff --git a/packages/core.gbapp/services/GBConversationalService.ts b/packages/core.gbapp/services/GBConversationalService.ts index f2be0931..2ef8e674 100644 --- a/packages/core.gbapp/services/GBConversationalService.ts +++ b/packages/core.gbapp/services/GBConversationalService.ts @@ -63,6 +63,7 @@ import TextToSpeechV1 from 'ibm-watson/text-to-speech/v1.js'; import { IamAuthenticator } from 'ibm-watson/auth/index.js'; import * as marked from 'marked'; import Translate from '@google-cloud/translate'; +import { List } from 'whatsapp-web.js'; /** * Provides basic services for handling messages and dispatching to back-end @@ -1087,8 +1088,10 @@ export class GBConversationalService { * * Sends a message in a user with an already started conversation (got ConversationReference set) */ - public async sendOnConversation(min: GBMinInstance, user: GuaribasUser, message: string) { - if (user.conversationReference.startsWith('spaces')) { + public async sendOnConversation(min: GBMinInstance, user: GuaribasUser, message: any) { + if (message['buttons'] || message['sections']) { + await min['whatsAppDirectLine'].sendToDevice(user.userSystemId, message, user.conversationReference ); + } else if (user.conversationReference.startsWith('spaces')) { await min['googleDirectLine'].sendToDevice(user.userSystemId, null, user.conversationReference, message); } else { const ref = JSON.parse(user.conversationReference); diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index 8119ab28..cd1e287c 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -45,12 +45,9 @@ import { GBMinService } from '../../core.gbapp/services/GBMinService.js'; import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; import qrcode from 'qrcode-terminal'; import express from 'express'; -import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; -import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; -import { method } from 'lodash'; -import pkg from 'whatsapp-web.js'; import { GBSSR } from '../../core.gbapp/services/GBSSR.js'; -const { Buttons, Client, MessageMedia } = pkg; +import pkg from 'whatsapp-web.js'; +const { List, Buttons, Client, MessageMedia } = pkg; /** * Support for Whatsapp. @@ -116,13 +113,17 @@ export class WhatsappDirectLine extends GBService { } } - public async sendButton() { - let url = ''; - const media = await MessageMedia.fromUrl(url); + public async sendButton(number) { + let url = 'https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b?ver=5c31'; + const media = await MessageMedia.fromUrl(url, {unsafeMime: true}); media.mimetype = 'image/png'; media.filename = 'hello.png'; - let btnClickableMenu = new Buttons(media as any, [{ id: 'customId', body: 'button1' }, { body: 'button2' }]); - await this.sendToDevice('', btnClickableMenu as any, null); + let btnClickableMenu = new Buttons(media as any, [ + { id: 'customId', body: 'button1' }, { body: 'button2' }, + { body: 'button3' }, { body: 'button4' }, + { body: 'button4' }, { body: 'button6' }, + ]); + await this.sendToDevice(number, btnClickableMenu as any, null); } public async setup(setUrl: boolean) { const client = await new SwaggerClient({ @@ -140,76 +141,72 @@ export class WhatsappDirectLine extends GBService { switch (this.provider) { case 'GeneralBots': const minBoot = GBServer.globals.minBoot; - // TODO: REMOVE THIS. - if (!setUrl) { - this.customClient = minBoot.whatsAppDirectLine.customClient; - } else { - // Initialize the browser using a local profile for each bot. - const gbaiName = `${this.min.botId}.gbai`; - const localName = Path.join('work', gbaiName, 'profile'); - const createClient = () => { - const client = (this.customClient = new Client({ - puppeteer: GBSSR.preparePuppeteer(localName) - })); - client.on( - 'message', - (async message => { - await this.WhatsAppCallback(message, null); - }).bind(this) - ); - client.on( - 'qr', - (async qr => { - const adminNumber = this.min.core.getParam(this.min.instance, 'Bot Admin Number', null); - const adminEmail = this.min.core.getParam(this.min.instance, 'Bot Admin E-mail', null); - // Sends QR Code to boot bot admin. - const msg = `Please, scan QR Code with for bot ${this.botId}.`; - GBLog.info(msg); - qrcode.generate(qr, { small: true, scale: 0.5 }); - // While handling other bots uses boot instance of this class to send QR Codes. - // const s = new DialogKeywords(min., null, null, null); - // const qrBuf = await s.getQRCode(qr); - // const gbaiName = `${this.min.botId}.gbai`; - // const localName = Path.join('work', gbaiName, 'cache', `qr${GBAdminService.getRndReadableIdentifier()}.png`); - // fs.writeFileSync(localName, qrBuf); - // const url = urlJoin( - // GBServer.globals.publicAddress, - // this.min.botId, - // 'cache', - // Path.basename(localName) - // ); - // GBServer.globals.minBoot.whatsAppDirectLine.sendFileToDevice(adminNumber, url, Path.basename(localName), msg); - // s.sendEmail(adminEmail, `Check your WhatsApp for bot ${this.botId}`, msg); - }).bind(this) - ); - client.on('authenticated', async () => { - GBLog.verbose(`GBWhatsApp: QR Code authenticated for ${this.botId}.`); + // Initialize the browser using a local profile for each bot. + const gbaiName = `${this.min.botId}.gbai`; + const localName = Path.join('work', gbaiName, 'profile'); + const createClient = () => { + const client = (this.customClient = new Client({ + puppeteer: GBSSR.preparePuppeteer(localName) + })); + client.on( + 'message', + (async message => { + await this.WhatsAppCallback(message, null); + }).bind(this) + ); + client.on( + 'qr', + (async qr => { + const adminNumber = this.min.core.getParam(this.min.instance, 'Bot Admin Number', null); + const adminEmail = this.min.core.getParam(this.min.instance, 'Bot Admin E-mail', null); + // Sends QR Code to boot bot admin. + const msg = `Please, scan QR Code with for bot ${this.botId}.`; + GBLog.info(msg); + qrcode.generate(qr, { small: true, scale: 0.5 }); + // While handling other bots uses boot instance of this class to send QR Codes. + // const s = new DialogKeywords(min., null, null, null); + // const qrBuf = await s.getQRCode(qr); + // const gbaiName = `${this.min.botId}.gbai`; + // const localName = Path.join('work', gbaiName, 'cache', `qr${GBAdminService.getRndReadableIdentifier()}.png`); + // fs.writeFileSync(localName, qrBuf); + // const url = urlJoin( + // GBServer.globals.publicAddress, + // this.min.botId, + // 'cache', + // Path.basename(localName) + // ); + // GBServer.globals.minBoot.whatsAppDirectLine.sendFileToDevice(adminNumber, url, Path.basename(localName), msg); + // s.sendEmail(adminEmail, `Check your WhatsApp for bot ${this.botId}`, msg); + }).bind(this) + ); + client.on('authenticated', async () => { + + GBLog.verbose(`GBWhatsApp: QR Code authenticated for ${this.botId}.`); + }); + client.on('ready', async () => { + GBLog.verbose(`GBWhatsApp: Emptying chat list for ${this.botId}...`); + + // Keeps the chat list cleaned. + const chats = await client.getChats(); + await CollectionUtil.asyncForEach(chats, async chat => { + const sleep = ms => { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + }; + const wait = Math.floor(Math.random() * 5000) + 1000; + await sleep(wait); + if (chat.isGroup) { + // await chat.clearMessages(); + } else if (!chat.pinned) { + // await chat.delete(); + } }); - client.on('ready', async () => { - GBLog.verbose(`GBWhatsApp: Emptying chat list for ${this.botId}...`); - - // Keeps the chat list cleaned. - const chats = await client.getChats(); - await CollectionUtil.asyncForEach(chats, async chat => { - const sleep = ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); - }; - const wait = Math.floor(Math.random() * 5000) + 1000; - await sleep(wait); - if (chat.isGroup) { - await chat.clearMessages(); - } else if (!chat.pinned) { - await chat.delete(); - } - }); - }); - client.initialize(); - }; - createClient.bind(this)(); - setUrl = false; - } + }); + client.initialize(); + }; + createClient.bind(this)(); + setUrl = false; break; case 'chatapi': options = { @@ -226,7 +223,7 @@ export class WhatsappDirectLine extends GBService { } }; break; - case 'chatapi': + case 'official': url = urlJoin(this.whatsappServiceUrl, 'webhook'); options = { method: 'POST',