From ac0b3f150e429ab592660d8835636cc2429d0aac Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Sat, 9 Mar 2024 14:02:17 -0300 Subject: [PATCH] new(WhatsApp.gblib): Official Twilio driver. --- packages/core.gbapp/services/GBCoreService.ts | 6 +- packages/core.gbapp/services/GBMinService.ts | 18 +- packages/gpt.gblib/services/ChatServices.ts | 2 + packages/kb.gbapp/dialogs/AskDialog.ts | 95 +---- .../services/WhatsappDirectLine.ts | 339 +++--------------- 5 files changed, 70 insertions(+), 390 deletions(-) diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index 637a3d9d..a1a9801a 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -680,8 +680,8 @@ ENDPOINT_UPDATE=true name = name.trim(); if (instance.params) { - - params = typeof (instance.params) === 'object' ? instance.params: JSON.parse(instance.params); + + params = typeof (instance.params) === 'object' ? instance.params : JSON.parse(instance.params); params = GBUtil.caseInsensitive(params); value = params ? params[name] : defaultValue; } @@ -708,7 +708,7 @@ ENDPOINT_UPDATE=true value = null; } - return value; + return value ?? defaultValue; } /** diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 6894fb94..977feb8b 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -258,6 +258,7 @@ export class GBMinService { * installing all BASIC artifacts from .gbdialog and OAuth2. */ public async mountBot(instance: IGBInstance) { + // Build bot adapter. const { min, adapter, conversationState } = await this.buildBotAdapter( @@ -439,16 +440,24 @@ export class GBMinService { this.createCheckHealthAddress(GBServer.globals.server, min, min.instance); + // Setups official handler for WhatsApp. + + GBServer.globals.server.post(`/${min.instance.botId}/whatsapp`, async (req, res) => { + const to = req.body.To.replace(/whatsapp\:\+/gi, ''); + const whatsAppDirectLine = WhatsappDirectLine.botsByNumber[to]; + await whatsAppDirectLine.WhatsAppCallback(req, res, whatsAppDirectLine.botId); + }).bind(min); + GBDeployer.mountGBKBAssets(`${botId}.gbkb`, botId, `${botId}.gbkb`); } - public static isChatAPI(req: any, res: any) { + public static getProviderName(req: any, res: any) { if (!res) { return 'GeneralBots'; } - if (req.NumMedia) + if (req.body?.AccountSid) { - return 'Official'; + return 'official'; } return req.body.phone_id ? 'maytapi' : 'chatapi'; } @@ -833,7 +842,8 @@ export class GBMinService { await min.whatsAppDirectLine.setup(true); } else { - if (min !== minBoot && minBoot.instance.whatsappServiceKey) { + if (min !== minBoot && minBoot.instance.whatsappServiceKey + && min.instance.webchatKey) { min.whatsAppDirectLine = new WhatsappDirectLine( min, min.botId, diff --git a/packages/gpt.gblib/services/ChatServices.ts b/packages/gpt.gblib/services/ChatServices.ts index 77f7c425..38440c9b 100644 --- a/packages/gpt.gblib/services/ChatServices.ts +++ b/packages/gpt.gblib/services/ChatServices.ts @@ -81,6 +81,8 @@ export class ChatServices { subjects: GuaribasSubject[] ) { + return { answer: undefined, questionId: 0 }; + if (!process.env.OPENAI_API_KEY) { return { answer: undefined, questionId: 0 }; } diff --git a/packages/kb.gbapp/dialogs/AskDialog.ts b/packages/kb.gbapp/dialogs/AskDialog.ts index 2abe9b70..09cb68e0 100644 --- a/packages/kb.gbapp/dialogs/AskDialog.ts +++ b/packages/kb.gbapp/dialogs/AskDialog.ts @@ -228,112 +228,27 @@ export class AskDialog extends IGBDialog { min.instance.searchScore ? min.instance.searchScore : minBoot.instance.searchScore ); - // TODO: https://github.com/GeneralBots/BotServer/issues/9 user.lastQuestion = text; - - const resultsA = await service.ask(min,step.context.activity['pid'], text, searchScore, null /* user.subjects */); + const results = await service.ask(min,step.context.activity['pid'], text, searchScore, null /* user.subjects */); // If there is some result, answer immediately. - if (resultsA !== undefined && resultsA.answer !== undefined) { - // Saves some context info. - - // user.isAsking = false; - // user.lastQuestionId = resultsA.questionId; + if (results !== undefined && results.answer !== undefined) { // Sends the answer to all outputs, including projector. - answer = resultsA.answer; + answer = results.answer; - // If this search was restricted to some subjects... - } - // TODO: https://github.com/GeneralBots/BotServer/issues/9 - // else if (user.subjects && user.subjects.length > 0) { - // // ..second time running Search, now with no filter. - - // const resultsB = await service.ask(min.instance, text, searchScore, undefined); - - // // If there is some result, answer immediately. - - // if (resultsB !== undefined && resultsB.answer !== undefined) { - // // Saves some context info. - - // // user2.isAsking = false; - // // user2.lastQuestionId = resultsB.questionId; - - // // Informs user that a broader search will be used. - - // if (user2.subjects.length > 0) { - // await min.conversationalService.sendText(min, step, Messages[locale].wider_answer); - // } - - // answer = resultsB.answer; - // } - // } - - // Try to answer by search. - - if (answer) { return await AskDialog.handleAnswer(service, min, step, user, answer); } - // Tries to answer by NLP. - - let handled = await min.conversationalService.routeNLP(step, min, text); - if (handled) { - return; - } - - // Tries to answer by Reading Comprehension. - - if (process.env.GBMODELS_SERVER) { - text = await min.conversationalService.translate(min, text, 'en'); - let answered = false; - - const docs = await min.kbService['getDocs'](min.instance.instanceId); - - await CollectionUtil.asyncForEach(docs, async (doc: GuaribasAnswer) => { - if (!answered) { - const answerText = await min.kbService['readComprehension'](min.instance.instanceId, doc.content, text); - answered = true; - text = await min.conversationalService.translate( - min, - text, - user.locale - ? user.locale - : min.core.getParam(min.instance, 'Locale', GBConfigService.get('LOCALE')) - ); - await min.conversationalService.sendText(min, step, answerText); - await min.conversationalService.sendEvent(min, step, 'stop', undefined); - } - }); - return await step.replaceDialog('/ask', { isReturning: true }); - } - if (process.env.OPENAI_EMAIL) { - if (!GBServer.globals.chatGPT) { - GBServer.globals.chatGPT = new ChatGPTAPIBrowser({ - email: process.env.OPENAI_EMAIL, - password: process.env.OPENAI_PASSWORD - }); - await GBServer.globals.chatGPT.init(); - } - const CHATGPT_TIMEOUT = 60 * 1000; - GBLog.info(`ChatGPT being used...`); - - const response = await GBServer.globals.chatGPT.sendMessage(text, { timeoutMs: CHATGPT_TIMEOUT }); - - if (!response) { - GBLog.info(`SEARCH called but NO answer could be found (zero results).`); - } else { - await min.conversationalService.sendText(min, step, response); - } - return await step.replaceDialog('/ask', { isReturning: true }); - } + GBLog.info(`SEARCH called but NO answer could be found (zero results).`); // Not found. const message = min.core.getParam(min.instance, 'Not Found Message', Messages[locale].did_not_find); await min.conversationalService.sendText(min, step, message); + return await step.replaceDialog('/ask', { isReturning: true }); } ]; diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index ca96c218..077adde0 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -105,12 +105,7 @@ export class WhatsappDirectLine extends GBService { this.whatsappServiceUrl = whatsappServiceUrl; this.provider = whatsappServiceKey === 'internal' - ? 'GeneralBots' - : whatsappServiceKey.indexOf('official') > -1 - ? 'official' - : whatsappServiceKey !== 'internal' - ? 'graphapi' - : 'chatapi'; + ? 'GeneralBots' : 'official'; this.groupId = groupId; } @@ -134,12 +129,12 @@ export class WhatsappDirectLine extends GBService { let options: any; switch (this.provider) { - case 'Official': + case 'official': const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; - this.customClient = twilio(accountSid, authToken); + this.customClient = twilio(null, authToken, { accountSid: accountSid }); - break; + break; case 'GeneralBots': const minBoot = GBServer.globals.minBoot; // Initialize the browser using a local profile for each bot. @@ -207,59 +202,6 @@ export class WhatsappDirectLine extends GBService { } setUrl = false; - break; - case 'chatapi': - options = { - method: 'POST', - url: urlJoin(this.whatsappServiceUrl, 'webhook'), - timeout: 10000, - qs: { - token: this.whatsappServiceKey, - webhookUrl: `${GBServer.globals.publicAddress}/webhooks/whatsapp/${this.botId}`, - set: true - }, - headers: { - 'cache-control': 'no-cache' - } - }; - break; - case 'official': - url = urlJoin(this.whatsappServiceUrl, 'webhook'); - options = { - method: 'POST', - url: url, - timeout: 10000, - qs: { - token: this.whatsappServiceKey, - webhookUrl: `${GBServer.globals.publicAddress}/webhooks/whatsapp/${this.botId}`, - set: true - }, - headers: { - 'cache-control': 'no-cache' - } - }; - - break; - case 'maytapi': - const phoneId = this.whatsappServiceNumber.split(';')[0]; - let productId = this.whatsappServiceNumber.split(';')[1]; - url = `${this.INSTANCE_URL}/${productId}/${phoneId}/config`; - body = { - webhook: `${GBServer.globals.publicAddress}/webhooks/whatsapp/${this.botId}`, - ack_delivery: false - }; - WhatsappDirectLine.phones[phoneId] = this.botId; - - options = { - url: url, - method: 'POST', - body: body, - headers: { - 'x-maytapi-key': this.whatsappServiceKey, - 'Content-Type': 'application/json' - }, - json: true - }; break; } @@ -301,7 +243,7 @@ export class WhatsappDirectLine extends GBService { } public static providerFromRequest(req: any) { - return req.body.messages ? 'chatapi' : req.body.message ? 'maytapi' : req.body.message ? 'graphapi' : 'GeneralBots'; + return req.body.ProfileName ? 'official' : 'GeneralBots'; } public async received(req, res) { @@ -313,6 +255,14 @@ export class WhatsappDirectLine extends GBService { let attachments = null; switch (provider) { + + case 'official': + message = req.body; + from = req.body.From.replace(/whatsapp\:\+/gi, ''); + to = req.body.To.replace(/whatsapp\:\+/gi, ''); + text = req.body.Body; + fromName = req.body.ProfileName; + break; case 'GeneralBots': message = req; to = message.to.endsWith('@g.us') ? message.to.split('@')[0] : message.to.split('@')[0]; @@ -355,44 +305,6 @@ export class WhatsappDirectLine extends GBService { } break; - - case 'chatapi': - message = req.body.messages[0]; - text = message.body; - from = req.body.messages[0].author.split('@')[0]; - fromName = req.body.messages[0].senderName; - - if (message.type !== 'chat') { - attachments = []; - attachments.push({ - name: 'uploaded', - contentType: 'application/octet-stream', - contentUrl: message.body - }); - } - - if (req.body.messages[0].fromMe) { - res.end(); - - return; // Exit here. - } - - break; - case 'graphapi': - break; - - case 'maytapi': - message = req.body.message; - text = message.text; - from = req.body.user.phone; - fromName = req.body.user.name; - - if (req.body.message.fromMe) { - res.end(); - - return; // Exit here. - } - break; } text = text.replace(/\@\d+ /gi, ''); @@ -406,11 +318,7 @@ export class WhatsappDirectLine extends GBService { botShortcuts = botShortcuts.split(' '); } - if (provider === 'chatapi') { - if (message.chatName.charAt(0) !== '+') { - group = message.chatName; - } - } else if (provider === 'GeneralBots') { + if (provider === 'GeneralBots') { if (message.from.endsWith('@g.us')) { group = message.from; } @@ -749,66 +657,6 @@ export class WhatsappDirectLine extends GBService { await this.customClient.sendMessage(to, attachment, { caption: caption }); break; - case 'chatapi': - options = { - method: 'POST', - url: urlJoin(this.whatsappServiceUrl, 'sendFile'), - qs: { - token: this.whatsappServiceKey - }, - json: true, - body: { - phone: to, - body: url, - filename: filename, - caption: caption - }, - headers: { - 'cache-control': 'no-cache' - } - }; - - break; - case 'maytapi': - let contents = 0; - let body = { - to_number: to, - type: 'media', - message: url, - text: caption - }; - - let phoneId = this.whatsappServiceNumber.split(';')[0]; - let productId = this.whatsappServiceNumber.split(';')[1]; - - options = { - url: `${this.INSTANCE_URL}/${productId}/${phoneId}/sendMessage`, - method: 'post', - json: true, - body, - headers: { - 'Content-Type': 'application/json', - 'x-maytapi-key': this.whatsappServiceKey - } - }; - - break; - - case 'graphapi': - url = `https://graph.facebook.com/v15.0/${phoneId}/messages`; - options = { - method: 'POST', - timeout: 10000, - headers: { - token: `Bearer `, - 'Content-Type': 'application/json' - }, - body: { - messaging_product: 'whatsapp', - recipient_type: 'individual', - to: phoneId - } - }; } if (options) { @@ -831,24 +679,6 @@ export class WhatsappDirectLine extends GBService { break; - case 'chatapi': - options = { - method: 'POST', - url: urlJoin(this.whatsappServiceUrl, 'sendPTT'), - qs: { - token: this.whatsappServiceKey, - phone: chatId ? null : to, - chatId: chatId, - body: url - }, - headers: { - 'cache-control': 'no-cache' - } - }; - - break; - case 'maytapi': - throw GBError.create('Sending audio in Maytapi not supported.'); } if (options) { @@ -881,19 +711,27 @@ export class WhatsappDirectLine extends GBService { switch (this.provider) { - case 'Official': + case 'official': const botNumber = this.min.core.getParam(this.min.instance, 'Bot Number', null); - if (to.charAt(0) !== '+') { + if (to.charAt(0) !== '+') { to = `+${to}` } - await this.customClient.messages - .create({ - body: msg, - from: `whatsapp:${botNumber}`, - to: `whatsapp:${to}` - // TODO: mediaUrl. - }); - + + let messages = msg.match(/(.|[\r\n]){1,1000}/g) + + await CollectionUtil.asyncForEach(messages, async msg => { + await GBUtil.sleep(3000); + await this.customClient.messages + .create({ + body: msg, + from: `whatsapp:${botNumber}`, + to: `whatsapp:${to}` + // TODO: mediaUrl. + }); + + }); + + break; case 'GeneralBots': to = to.replace('+', ''); @@ -913,38 +751,6 @@ export class WhatsappDirectLine extends GBService { break; - case 'chatapi': - options = { - method: 'POST', - url: urlJoin(this.whatsappServiceUrl, 'message'), - qs: { - token: this.whatsappServiceKey, - phone: chatId ? null : to, - chatId: chatId, - body: msg - }, - headers: { - 'cache-control': 'no-cache' - } - }; - break; - case 'maytapi': - let phoneId = this.whatsappServiceNumber.split(';')[0]; - let productId = this.whatsappServiceNumber.split(';')[1]; - url = `${this.INSTANCE_URL}/${productId}/${phoneId}/sendMessage`; - - options = { - method: 'post', - json: true, - body: { type: 'text', message: msg, to_number: to }, - headers: { - 'Content-Type': 'application/json', - 'x-maytapi-key': this.whatsappServiceKey - } - }; - break; - case 'graphapi': - break; } if (options) { @@ -963,27 +769,28 @@ export class WhatsappDirectLine extends GBService { await this.sendToDevice(to, text, conversationId); } - private async WhatsAppCallback(req, res) { + private async WhatsAppCallback(req, res, botId = null) { try { if (!req.body && req.type !== 'ptt') { return; } - let provider = GBMinService.isChatAPI(req, res); + let provider = GBMinService.getProviderName(req, res); let id; let senderName; - let botId; let text; switch (provider) { - case 'Official': + case 'official': - const { body } = req; + const { body } = req; - let message; - - break; + id = body.From.replace(/whatsapp\:\+/, ''); + senderName = body.ProfileName; + text = body.Body; + + break; case 'GeneralBots': // Ignore E2E messages and status updates. @@ -995,47 +802,8 @@ export class WhatsappDirectLine extends GBService { id = req.from.split('@')[0]; senderName = req._data.notifyName; text = req.body; - botId = this.botId; break; - case 'chatapi': - if (req.body.ack) { - res.status(200); - res.end(); - - return; - } - if (req.body.messages[0].fromMe) { - res.end(); - - return; // Exit here. - } - id = req.body.messages[0].author.split('@')[0]; - senderName = req.body.messages[0].senderName; - text = req.body.messages[0].body; - botId = req.params.botId; - if (botId === '[default]' || botId === undefined) { - botId = GBConfigService.get('BOT_ID'); - } - break; - case 'maytapi': - if (req.body.type !== 'message') { - res.status(200); - res.end(); - - return; - } - if (req.body.message.fromMe) { - res.end(); - - return; // Exit here. - } - id = req.body.user.phone; - senderName = req.body.user.name; - text = req.body.message.text; - - botId = WhatsappDirectLine.phones[req.body.phoneId]; - break; } const sec = new SecService(); @@ -1057,7 +825,7 @@ export class WhatsappDirectLine extends GBService { const botNumber = urlMin ? urlMin.core.getParam(urlMin.instance, 'Bot Number', null) : null; if (botNumber && GBServer.globals.minBoot.botId !== urlMin.botId) { GBLog.info(`${id} fixed by bot number talked to: ${botId}.`); - let locale = user?.locale ? user.locale: min.core.getParam( + let locale = user?.locale ? user.locale : min.core.getParam( min.instance, 'Default User Language', GBConfigService.get('DEFAULT_USER_LANGUAGE')); @@ -1096,7 +864,7 @@ export class WhatsappDirectLine extends GBService { buf, user.locale ); - + req.body = text; } else { @@ -1113,14 +881,7 @@ export class WhatsappDirectLine extends GBService { text = text.replace(/\@\d+ /gi, ''); let group; - if (provider === 'chatapi') { - // Ensures that the bot group is the active bot for the user (like switching). - - const message = req.body.messages[0]; - if (message.chatName.charAt(0) !== '+') { - group = message.chatName; - } - } else if (provider === 'GeneralBots') { + if (provider === 'GeneralBots') { // Ensures that the bot group is the active bot for the user (like switching). const message = req; @@ -1185,11 +946,7 @@ export class WhatsappDirectLine extends GBService { if (startDialog) { GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`); - if (provider === 'chatapi') { - req.body.messages[0].body = `/start`; - } else if (provider === 'maytapi') { - req.body.message = `/start`; - } else { + if (provider === 'GeneralBots') { req.body = `/start`; } @@ -1221,11 +978,7 @@ export class WhatsappDirectLine extends GBService { if (startDialog) { GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`); - if (provider === 'chatapi') { - req.body.messages[0].body = `/start`; - } else if (provider === 'maytapi') { - req.body.message = `/start`; - } else { + if (provider === 'GeneralBots') { req.body = `/start`; }