From 3bb4217a45103fe5a8edb02d8fbb95f5e1db5f17 Mon Sep 17 00:00:00 2001 From: w3nder Date: Sun, 24 Dec 2023 14:32:05 -0300 Subject: [PATCH 01/36] =?UTF-8?q?fix:=20Corre=C3=A7=C3=A3o=20na=20Fun?= =?UTF-8?q?=C3=A7=C3=A3o=20sendList=20Ao=20disparar=20uma=20lista=20de=20m?= =?UTF-8?q?ensagens,=20agora=20enviamos=20elas=20com=20o=20tipo=20'PRODUCT?= =?UTF-8?q?=5FLIST'=20e=20realizamos=20a=20corre=C3=A7=C3=A3o=20necess?= =?UTF-8?q?=C3=A1ria=20na=20fun=C3=A7=C3=A3o=20patchMessageBeforeSending?= =?UTF-8?q?=20para=20o=20tipo=20'SINGLE=5FSELECT'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/whatsapp/services/whatsapp.service.ts | 56 ++++++++++------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f222d5b7..9b9d0234 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1413,22 +1413,19 @@ export class WAStartupService { syncFullHistory: false, userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; + patchMessageBeforeSending(message) { + if (message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { + message = JSON.parse(JSON.stringify(message)); + + message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + + if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { + message = JSON.parse(JSON.stringify(message)); + + message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + } + return message; }, }; @@ -1500,22 +1497,19 @@ export class WAStartupService { syncFullHistory: false, userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; + patchMessageBeforeSending(message) { + if (message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { + message = JSON.parse(JSON.stringify(message)); + + message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + + if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { + message = JSON.parse(JSON.stringify(message)); + + message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; + } + return message; }, }; @@ -3012,7 +3006,7 @@ export class WAStartupService { buttonText: data.listMessage?.buttonText, footerText: data.listMessage?.footerText, sections: data.listMessage.sections, - listType: 1, + listType: 2, }, }, data?.options, From 82894a1c4f70cee9e9f074c2c2a8d5bec97cbd81 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Mon, 25 Dec 2023 18:48:25 -0300 Subject: [PATCH 02/36] refactor: change the message ids cache in chatwoot to use a in memory cache Remove use of disc cache for optimize performance. To make this, we need change to use only one instance of ChatwootService in entire application. --- .../controllers/chatwoot.controller.ts | 4 +- .../controllers/instance.controller.ts | 6 ++ src/whatsapp/services/chatwoot.service.ts | 102 ++++-------------- src/whatsapp/services/whatsapp.service.ts | 4 +- 4 files changed, 32 insertions(+), 84 deletions(-) diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 8f59ccac..d1090956 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -7,7 +7,7 @@ import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { ChatwootService } from '../services/chatwoot.service'; -import { waMonitor } from '../whatsapp.module'; +import { instanceController } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); @@ -94,7 +94,7 @@ export class ChatwootController { public async receiveWebhook(instance: InstanceDto, data: any) { logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); + const chatwootService = instanceController.getChatwootService(); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0f06895e..d15e5c2b 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -659,4 +659,10 @@ export class InstanceController { this.logger.verbose('requested refreshToken'); return await this.authService.refreshToken(oldToken); } + + public getChatwootService() { + this.logger.verbose('getting chatwootService object instance'); + + return this.chatwootService; + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 26b0cce9..cf173050 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,14 +1,13 @@ import ChatwootClient from '@figuro/chatwoot-sdk'; import axios from 'axios'; import FormData from 'form-data'; -import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { createReadStream, unlinkSync, writeFileSync } from 'fs'; import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; -import { ROOT_DIR } from '../../config/path.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; @@ -18,8 +17,7 @@ import { Events } from '../types/wa.types'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; + private messageCache: Record>; private readonly logger = new Logger(ChatwootService.name); @@ -30,31 +28,25 @@ export class ChatwootService { private readonly configService: ConfigService, private readonly repository: RepositoryBroker, ) { - this.messageCache = new Set(); + this.messageCache = {}; } - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); + private isMessageInCache(instance: InstanceDto, id: number) { + this.logger.verbose('check if message is in cache'); + if (!this.messageCache[instance.instanceName]) { + return false; } + + return this.messageCache[instance.instanceName].has(id); } - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); - } + private addMessageToCache(instance: InstanceDto, id: number) { + this.logger.verbose('add message to cache'); - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); + if (!this.messageCache[instance.instanceName]) { + this.messageCache[instance.instanceName] = new Set(); + } + this.messageCache[instance.instanceName].add(id); } private async getProvider(instance: InstanceDto) { @@ -1105,22 +1097,11 @@ export class ChatwootService { if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { this.logger.verbose('check if is group'); - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - this.logger.verbose('cache file path: ' + this.messageCacheFile); - - this.messageCache = this.loadMessageCache(); - this.logger.verbose('cache file loaded'); - this.logger.verbose(this.messageCache); - - this.logger.verbose('check if message is cached'); - if (this.messageCache.has(body.id.toString())) { + if (this.isMessageInCache(instance, body.id)) { this.logger.verbose('message is cached'); return { message: 'bot' }; } - this.logger.verbose('clear cache'); - this.clearMessageCache(); - this.logger.verbose('Format message to send'); let formatText: string; if (senderName === null || senderName === undefined) { @@ -1597,14 +1578,7 @@ export class ChatwootService { return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); return send; } else { @@ -1618,14 +1592,7 @@ export class ChatwootService { return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); return send; } @@ -1650,11 +1617,7 @@ export class ChatwootService { this.logger.warn('message not sent'); return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - this.messageCache = this.loadMessageCache(); - this.messageCache.add(send.id.toString()); - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); } return; @@ -1711,14 +1674,7 @@ export class ChatwootService { return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); return send; } @@ -1746,14 +1702,7 @@ export class ChatwootService { return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); return send; } else { @@ -1767,14 +1716,7 @@ export class ChatwootService { return; } - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); + this.addMessageToCache(instance, send.id); return send; } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f222d5b7..ce355486 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -75,6 +75,7 @@ import { getIO } from '../../libs/socket.server'; import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; +import { instanceController } from '../../whatsapp/whatsapp.module'; import { ArchiveChatDto, DeleteMessage, @@ -131,7 +132,6 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; import { ChamaaiService } from './chamaai.service'; -import { ChatwootService } from './chatwoot.service'; import { TypebotService } from './typebot.service'; const retryCache = {}; @@ -169,7 +169,7 @@ export class WAStartupService { private phoneNumber: string; - private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); + private chatwootService = instanceController.getChatwootService(); private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); From 74308970854cab6bd24fea95b904ff70e084ba6a Mon Sep 17 00:00:00 2001 From: jaison-x Date: Mon, 25 Dec 2023 23:58:07 -0300 Subject: [PATCH 03/36] fix: when receiving a file from whatsapp, use the original filename in chatwoot when possible --- src/whatsapp/services/chatwoot.service.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 26b0cce9..fab26a87 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1561,8 +1561,15 @@ export class ChatwootService { }, }); - const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + let prependFilename: string; + if (body?.message[body?.messageType]?.fileName) { + prependFilename = `${path.parse(body.message[body.messageType].fileName).name}-${Math.floor( + Math.random() * (99 - 10 + 1) + 10, + )}`; + } else { + prependFilename = Math.random().toString(36).substring(7); + } + const nameFile = `${prependFilename}.${mimeTypes.extension(downloadBase64.mimetype)}`; const fileData = Buffer.from(downloadBase64.base64, 'base64'); From 49aa1ea17cd7f73828cb1679c62e97dcee8388f4 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Tue, 26 Dec 2023 01:21:05 -0300 Subject: [PATCH 04/36] fix: when receiving a file from whatsapp, use the original filename in chatwoot when possible --- src/whatsapp/services/chatwoot.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index fab26a87..d91b27ba 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1562,10 +1562,15 @@ export class ChatwootService { }); let prependFilename: string; - if (body?.message[body?.messageType]?.fileName) { - prependFilename = `${path.parse(body.message[body.messageType].fileName).name}-${Math.floor( - Math.random() * (99 - 10 + 1) + 10, - )}`; + if ( + body?.message[body?.messageType]?.fileName || + body?.message[body?.messageType]?.message?.documentMessage?.fileName + ) { + prependFilename = path.parse( + body?.message[body?.messageType]?.fileName || + body?.message[body?.messageType]?.message?.documentMessage?.fileName, + ).name; + prependFilename += `-${Math.floor(Math.random() * (99 - 10 + 1) + 10)}`; } else { prependFilename = Math.random().toString(36).substring(7); } From 8b5f73badd200601f689842a8c96b27b2fd5ce29 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Wed, 27 Dec 2023 13:56:32 -0300 Subject: [PATCH 05/36] fix: added support to use source_id to check chatwoot's webhook needs to be ignored. With this, messageCache is used to support Chatwoot version <= 3.3.1. After this version we can remove use of message cache and use only the source_id field to check chatwoot's webhook needs to be ignored. It's have much better performance. --- src/whatsapp/services/chatwoot.service.ts | 71 ++++++++++++++++++++--- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index cf173050..c62597f7 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -28,16 +28,24 @@ export class ChatwootService { private readonly configService: ConfigService, private readonly repository: RepositoryBroker, ) { + // messageCache is used to support Chatwoot version <= 3.3.1. + // after this version we can remove use of message cache and use source_id to check webhook needs to be ignored this.messageCache = {}; } - private isMessageInCache(instance: InstanceDto, id: number) { + private isMessageInCache(instance: InstanceDto, id: number, remove = true) { this.logger.verbose('check if message is in cache'); if (!this.messageCache[instance.instanceName]) { return false; } - return this.messageCache[instance.instanceName].has(id); + const hasId = this.messageCache[instance.instanceName].has(id); + + if (remove) { + this.messageCache[instance.instanceName].delete(id); + } + + return hasId; } private addMessageToCache(instance: InstanceDto, id: number) { @@ -636,6 +644,7 @@ export class ChatwootService { filename: string; }[], messageBody?: any, + sourceId?: string, ) { this.logger.verbose('create message to instance: ' + instance.instanceName); @@ -657,6 +666,7 @@ export class ChatwootService { message_type: messageType, attachments: attachments, private: privateMessage || false, + source_id: sourceId, content_attributes: { ...replyToIds, }, @@ -757,6 +767,7 @@ export class ChatwootService { content?: string, instance?: InstanceDto, messageBody?: any, + sourceId?: string, ) { this.logger.verbose('send data to chatwoot'); @@ -783,6 +794,10 @@ export class ChatwootService { } } + if (sourceId) { + data.append('source_id', sourceId); + } + this.logger.verbose('get client to instance: ' + this.provider.instanceName); const config = { method: 'post', @@ -1097,7 +1112,13 @@ export class ChatwootService { if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { this.logger.verbose('check if is group'); - if (this.isMessageInCache(instance, body.id)) { + // messageCache is used to support Chatwoot version <= 3.3.1. + // after this version we can remove use of message cache and use only source_id value check + // use of source_id is better for performance + if ( + body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' || + this.isMessageInCache(instance, body.id) + ) { this.logger.verbose('message is cached'); return { message: 'bot' }; } @@ -1571,7 +1592,15 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, content, instance, body); + const send = await this.sendData( + getConversation, + fileName, + messageType, + content, + instance, + body, + 'WAID:' + body.key.id, + ); if (!send) { this.logger.warn('message not sent'); @@ -1585,7 +1614,15 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, bodyMessage, instance, body); + const send = await this.sendData( + getConversation, + fileName, + messageType, + bodyMessage, + instance, + body, + 'WAID:' + body.key.id, + ); if (!send) { this.logger.warn('message not sent'); @@ -1612,6 +1649,7 @@ export class ChatwootService { { message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } }, }, + 'WAID:' + body.key.id, ); if (!send) { this.logger.warn('message not sent'); @@ -1667,6 +1705,7 @@ export class ChatwootService { `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, instance, body, + 'WAID:' + body.key.id, ); if (!send) { @@ -1695,7 +1734,16 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); + const send = await this.createMessage( + instance, + getConversation, + content, + messageType, + false, + [], + body, + 'WAID:' + body.key.id, + ); if (!send) { this.logger.warn('message not sent'); @@ -1709,7 +1757,16 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); + const send = await this.createMessage( + instance, + getConversation, + bodyMessage, + messageType, + false, + [], + body, + 'WAID:' + body.key.id, + ); if (!send) { this.logger.warn('message not sent'); From 7c2a8c0abbf379b20cf38daf90cd83b96330954b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 10:35:41 -0300 Subject: [PATCH 06/36] fix: Proxy configuration improvements --- CHANGELOG.md | 7 +++ src/validate/validate.schema.ts | 13 +++++- .../controllers/instance.controller.ts | 21 --------- src/whatsapp/controllers/proxy.controller.ts | 46 ++++++++++++++++++- src/whatsapp/dto/proxy.dto.ts | 10 +++- src/whatsapp/models/proxy.model.ts | 18 +++++++- src/whatsapp/services/proxy.service.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 16 +++++-- src/whatsapp/types/wa.types.ts | 10 +++- src/whatsapp/whatsapp.module.ts | 1 - 10 files changed, 111 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7f016d..d8a694c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.6.2 (develop) + +### Fixed + +* Proxy configuration improvements +* Correction in sending lists + # 1.6.1 (2023-12-22 11:43) ### Fixed diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 4b803873..1ccdf125 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -1086,7 +1086,18 @@ export const proxySchema: JSONSchema7 = { type: 'object', properties: { enabled: { type: 'boolean', enum: [true, false] }, - proxy: { type: 'string' }, + proxy: { + type: 'object', + properties: { + host: { type: 'string' }, + port: { type: 'string' }, + protocol: { type: 'string' }, + username: { type: 'string' }, + password: { type: 'string' }, + }, + required: ['host', 'port', 'protocol'], + ...isNotEmpty('host', 'port', 'protocol'), + }, }, required: ['enabled', 'proxy'], ...isNotEmpty('enabled', 'proxy'), diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0f06895e..0701742c 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -12,7 +12,6 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.service'; import { ChatwootService } from '../services/chatwoot.service'; import { WAMonitoringService } from '../services/monitor.service'; -import { ProxyService } from '../services/proxy.service'; import { RabbitmqService } from '../services/rabbitmq.service'; import { SettingsService } from '../services/settings.service'; import { SqsService } from '../services/sqs.service'; @@ -34,7 +33,6 @@ export class InstanceController { private readonly settingsService: SettingsService, private readonly websocketService: WebsocketService, private readonly rabbitmqService: RabbitmqService, - private readonly proxyService: ProxyService, private readonly sqsService: SqsService, private readonly typebotService: TypebotService, private readonly cache: RedisCache, @@ -76,7 +74,6 @@ export class InstanceController { typebot_delay_message, typebot_unknown_message, typebot_listening_from_me, - proxy, }: InstanceDto) { try { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -259,22 +256,6 @@ export class InstanceController { } } - if (proxy) { - this.logger.verbose('creating proxy'); - try { - this.proxyService.create( - instance, - { - enabled: true, - proxy, - }, - false, - ); - } catch (error) { - this.logger.log(error); - } - } - let sqsEvents: string[]; if (sqs_enabled) { @@ -406,7 +387,6 @@ export class InstanceController { }, settings, qrcode: getQrcode, - proxy, }; this.logger.verbose('instance created'); @@ -510,7 +490,6 @@ export class InstanceController { name_inbox: instance.instanceName, webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, }, - proxy, }; } catch (error) { this.logger.error(error.message[0]); diff --git a/src/whatsapp/controllers/proxy.controller.ts b/src/whatsapp/controllers/proxy.controller.ts index 1656d830..555c5975 100644 --- a/src/whatsapp/controllers/proxy.controller.ts +++ b/src/whatsapp/controllers/proxy.controller.ts @@ -1,4 +1,7 @@ +import axios from 'axios'; + import { Logger } from '../../config/logger.config'; +import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { ProxyDto } from '../dto/proxy.dto'; import { ProxyService } from '../services/proxy.service'; @@ -13,7 +16,16 @@ export class ProxyController { if (!data.enabled) { logger.verbose('proxy disabled'); - data.proxy = ''; + data.proxy = null; + } + + if (data.proxy) { + logger.verbose('proxy enabled'); + const { host, port, protocol, username, password } = data.proxy; + const testProxy = await this.testProxy(host, port, protocol, username, password); + if (!testProxy) { + throw new BadRequestException('Invalid proxy'); + } } return this.proxyService.create(instance, data); @@ -23,4 +35,36 @@ export class ProxyController { logger.verbose('requested findProxy from ' + instance.instanceName + ' instance'); return this.proxyService.find(instance); } + + private async testProxy(host: string, port: string, protocol: string, username?: string, password?: string) { + logger.verbose('requested testProxy'); + try { + let proxyConfig: any = { + host: host, + port: parseInt(port), + protocol: protocol, + }; + + if (username && password) { + proxyConfig = { + ...proxyConfig, + auth: { + username: username, + password: password, + }, + }; + } + const serverIp = await axios.get('http://meuip.com/api/meuip.php'); + + const response = await axios.get('http://meuip.com/api/meuip.php', { + proxy: proxyConfig, + }); + + logger.verbose('testProxy response: ' + response.data); + return response.data !== serverIp.data; + } catch (error) { + logger.error('testProxy error: ' + error); + return false; + } + } } diff --git a/src/whatsapp/dto/proxy.dto.ts b/src/whatsapp/dto/proxy.dto.ts index 0b6b2e70..7f3e7c06 100644 --- a/src/whatsapp/dto/proxy.dto.ts +++ b/src/whatsapp/dto/proxy.dto.ts @@ -1,4 +1,12 @@ +class Proxy { + host: string; + port: string; + protocol: string; + username?: string; + password?: string; +} + export class ProxyDto { enabled: boolean; - proxy: string; + proxy: Proxy; } diff --git a/src/whatsapp/models/proxy.model.ts b/src/whatsapp/models/proxy.model.ts index 3dea4f0c..4096f58f 100644 --- a/src/whatsapp/models/proxy.model.ts +++ b/src/whatsapp/models/proxy.model.ts @@ -2,16 +2,30 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../libs/db.connect'; +class Proxy { + host?: string; + port?: string; + protocol?: string; + username?: string; + password?: string; +} + export class ProxyRaw { _id?: string; enabled?: boolean; - proxy?: string; + proxy?: Proxy; } const proxySchema = new Schema({ _id: { type: String, _id: true }, enabled: { type: Boolean, required: true }, - proxy: { type: String, required: true }, + proxy: { + host: { type: String, required: true }, + port: { type: String, required: true }, + protocol: { type: String, required: true }, + username: { type: String, required: false }, + password: { type: String, required: false }, + }, }); export const ProxyModel = dbserver?.model(ProxyRaw.name, proxySchema, 'proxy'); diff --git a/src/whatsapp/services/proxy.service.ts b/src/whatsapp/services/proxy.service.ts index 1039fd5c..66dc5342 100644 --- a/src/whatsapp/services/proxy.service.ts +++ b/src/whatsapp/services/proxy.service.ts @@ -27,7 +27,7 @@ export class ProxyService { return result; } catch (error) { - return { enabled: false, proxy: '' }; + return { enabled: false, proxy: null }; } } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f222d5b7..98a6b067 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1369,8 +1369,8 @@ export class WAStartupService { if (this.localProxy.enabled) { this.logger.info('Proxy enabled: ' + this.localProxy.proxy); - if (this.localProxy.proxy.includes('proxyscrape')) { - const response = await axios.get(this.localProxy.proxy); + if (this.localProxy.proxy.host.includes('proxyscrape')) { + const response = await axios.get(this.localProxy.proxy.host); const text = response.data; const proxyUrls = text.split('\r\n'); const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); @@ -1379,8 +1379,15 @@ export class WAStartupService { agent: new ProxyAgent(proxyUrl as any), }; } else { + let proxyUri = + this.localProxy.proxy.protocol + '://' + this.localProxy.proxy.host + ':' + this.localProxy.proxy.port; + + if (this.localProxy.proxy.username && this.localProxy.proxy.password) { + proxyUri = `${this.localProxy.proxy.username}:${this.localProxy.proxy.password}@${proxyUri}`; + } + options = { - agent: new ProxyAgent(this.localProxy.proxy as any), + agent: new ProxyAgent(proxyUri as any), }; } } @@ -1903,7 +1910,8 @@ export class WAStartupService { this.logger.verbose('group ignored'); return; } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + // if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + if (key.remoteJid !== 'status@broadcast') { this.logger.verbose('Message update is valid'); let pollUpdates: any; diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 5adf9ca2..9f3cb890 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -109,9 +109,17 @@ export declare namespace wa { sessions?: Session[]; }; + type Proxy = { + host?: string; + port?: string; + protocol?: string; + username?: string; + password?: string; + }; + export type LocalProxy = { enabled?: boolean; - proxy?: string; + proxy?: Proxy; }; export type LocalChamaai = { diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index e89d4f56..d459bf6a 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -148,7 +148,6 @@ export const instanceController = new InstanceController( settingsService, websocketService, rabbitmqService, - proxyService, sqsService, typebotService, cache, From 3238150b925dcc83b7143a74005323e5903a6059 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 10:36:23 -0300 Subject: [PATCH 07/36] fix: Proxy configuration improvements --- src/whatsapp/services/whatsapp.service.ts | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 4cb8963a..b78125f5 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1421,18 +1421,21 @@ export class WAStartupService { userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending(message) { - if (message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { + if ( + message.deviceSentMessage?.message?.listMessage?.listType === + proto.Message.ListMessage.ListType.PRODUCT_LIST + ) { message = JSON.parse(JSON.stringify(message)); - + message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { message = JSON.parse(JSON.stringify(message)); - + message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + return message; }, }; @@ -1505,18 +1508,21 @@ export class WAStartupService { userDevicesCache: this.userDevicesCache, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending(message) { - if (message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST) { + if ( + message.deviceSentMessage?.message?.listMessage?.listType === + proto.Message.ListMessage.ListType.PRODUCT_LIST + ) { message = JSON.parse(JSON.stringify(message)); - + message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) { message = JSON.parse(JSON.stringify(message)); - + message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT; } - + return message; }, }; From bfa7d429bd1d2f2cfea4d486624407d72ad9bf0f Mon Sep 17 00:00:00 2001 From: jaison-x Date: Thu, 28 Dec 2023 14:43:50 -0300 Subject: [PATCH 08/36] refactor(chatwoot): remove message ids cache in chatwoot to use chatwoot's api itself. Remove use of disc cache to optimize performance. BREAKING CHANGE: to make this, we need to use the param `source_id` from message in chatwoot. This param is only available from api in chatwoot version => 3.4.0. --- .../controllers/chatwoot.controller.ts | 4 +- .../controllers/instance.controller.ts | 6 --- src/whatsapp/services/chatwoot.service.ts | 53 ++----------------- src/whatsapp/services/whatsapp.service.ts | 4 +- 4 files changed, 7 insertions(+), 60 deletions(-) diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index d1090956..8f59ccac 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -7,7 +7,7 @@ import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { ChatwootService } from '../services/chatwoot.service'; -import { instanceController } from '../whatsapp.module'; +import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); @@ -94,7 +94,7 @@ export class ChatwootController { public async receiveWebhook(instance: InstanceDto, data: any) { logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = instanceController.getChatwootService(); + const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index d15e5c2b..0f06895e 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -659,10 +659,4 @@ export class InstanceController { this.logger.verbose('requested refreshToken'); return await this.authService.refreshToken(oldToken); } - - public getChatwootService() { - this.logger.verbose('getting chatwootService object instance'); - - return this.chatwootService; - } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c62597f7..63c04b6f 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -17,8 +17,6 @@ import { Events } from '../types/wa.types'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { - private messageCache: Record>; - private readonly logger = new Logger(ChatwootService.name); private provider: any; @@ -27,35 +25,7 @@ export class ChatwootService { private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService, private readonly repository: RepositoryBroker, - ) { - // messageCache is used to support Chatwoot version <= 3.3.1. - // after this version we can remove use of message cache and use source_id to check webhook needs to be ignored - this.messageCache = {}; - } - - private isMessageInCache(instance: InstanceDto, id: number, remove = true) { - this.logger.verbose('check if message is in cache'); - if (!this.messageCache[instance.instanceName]) { - return false; - } - - const hasId = this.messageCache[instance.instanceName].has(id); - - if (remove) { - this.messageCache[instance.instanceName].delete(id); - } - - return hasId; - } - - private addMessageToCache(instance: InstanceDto, id: number) { - this.logger.verbose('add message to cache'); - - if (!this.messageCache[instance.instanceName]) { - this.messageCache[instance.instanceName] = new Set(); - } - this.messageCache[instance.instanceName].add(id); - } + ) {} private async getProvider(instance: InstanceDto) { this.logger.verbose('get provider to instance: ' + instance.instanceName); @@ -1112,14 +1082,8 @@ export class ChatwootService { if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { this.logger.verbose('check if is group'); - // messageCache is used to support Chatwoot version <= 3.3.1. - // after this version we can remove use of message cache and use only source_id value check - // use of source_id is better for performance - if ( - body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' || - this.isMessageInCache(instance, body.id) - ) { - this.logger.verbose('message is cached'); + if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') { + this.logger.verbose('message sent directly from whatsapp. Webhook ignored.'); return { message: 'bot' }; } @@ -1607,8 +1571,6 @@ export class ChatwootService { return; } - this.addMessageToCache(instance, send.id); - return send; } else { this.logger.verbose('message is not group'); @@ -1629,8 +1591,6 @@ export class ChatwootService { return; } - this.addMessageToCache(instance, send.id); - return send; } } @@ -1655,7 +1615,6 @@ export class ChatwootService { this.logger.warn('message not sent'); return; } - this.addMessageToCache(instance, send.id); } return; @@ -1713,8 +1672,6 @@ export class ChatwootService { return; } - this.addMessageToCache(instance, send.id); - return send; } @@ -1750,8 +1707,6 @@ export class ChatwootService { return; } - this.addMessageToCache(instance, send.id); - return send; } else { this.logger.verbose('message is not group'); @@ -1773,8 +1728,6 @@ export class ChatwootService { return; } - this.addMessageToCache(instance, send.id); - return send; } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index ce355486..f222d5b7 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -75,7 +75,6 @@ import { getIO } from '../../libs/socket.server'; import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; -import { instanceController } from '../../whatsapp/whatsapp.module'; import { ArchiveChatDto, DeleteMessage, @@ -132,6 +131,7 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; import { ChamaaiService } from './chamaai.service'; +import { ChatwootService } from './chatwoot.service'; import { TypebotService } from './typebot.service'; const retryCache = {}; @@ -169,7 +169,7 @@ export class WAStartupService { private phoneNumber: string; - private chatwootService = instanceController.getChatwootService(); + private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); From 2aadd1cac59d220a71c47cbd7d398d8a9a17e752 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 17:32:19 -0300 Subject: [PATCH 09/36] fix: Adjust in webhook_base64 --- CHANGELOG.md | 1 + src/whatsapp/services/whatsapp.service.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a694c6..18b790e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Proxy configuration improvements * Correction in sending lists +* Adjust in webhook_base64 # 1.6.1 (2023-12-22 11:43) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index b78125f5..7f247b0d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1748,10 +1748,14 @@ export class WAStartupService { let messageRaw: MessageRaw; - if ( - (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || - received?.message?.imageMessage - ) { + const isMedia = + received?.message?.imageMessage || + received?.message?.videoMessage || + received?.message?.stickerMessage || + received?.message?.documentMessage || + received?.message?.audioMessage; + + if (this.localWebhook.webhook_base64 === true && isMedia) { const buffer = await downloadMediaMessage( { key: received.key, message: received?.message }, 'buffer', From 6983f385fcf6725115e03bc584a0736cf9f5e703 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 18:07:24 -0300 Subject: [PATCH 10/36] fix: correction in typebot text formatting --- src/whatsapp/services/typebot.service.ts | 92 +++++++++++------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index 3dc4c894..e4e36310 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -389,6 +389,7 @@ export class TypebotService { input, clientSideActions, this.eventEmitter, + applyFormatting, ).catch((err) => { console.error('Erro ao processar mensagens:', err); }); @@ -404,72 +405,64 @@ export class TypebotService { return null; } - async function processMessages(instance, messages, input, clientSideActions, eventEmitter) { + function applyFormatting(element) { + let text = ''; + + if (element.text) { + text += element.text; + } + + if (element.type === 'p' || element.type === 'inline-variable') { + for (const child of element.children) { + text += applyFormatting(child); + } + } + + let formats = ''; + + if (element.bold) { + formats += '*'; + } + + if (element.italic) { + formats += '_'; + } + + if (element.underline) { + formats += '~'; + } + + let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`; + + if (element.url) { + const linkText = element.children[0]?.text || ''; + formattedText = `[${linkText}](${element.url})`; + } + + return formattedText; + } + + async function processMessages(instance, messages, input, clientSideActions, eventEmitter, applyFormatting) { for (const message of messages) { const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); if (message.type === 'text') { let formattedText = ''; - let linkPreview = false; - for (const richText of message.content.richText) { - if (richText.type === 'variable') { - for (const child of richText.children) { - for (const grandChild of child.children) { - formattedText += grandChild.text; - } - } - } else { - for (const element of richText.children) { - let text = ''; - - if (element.type === 'inline-variable') { - for (const child of element.children) { - for (const grandChild of child.children) { - text += grandChild.text; - } - } - } else if (element.text) { - text = element.text; - } - - // if (element.text) { - // text = element.text; - // } - - if (element.bold) { - text = `*${text}*`; - } - - if (element.italic) { - text = `_${text}_`; - } - - if (element.underline) { - text = `*${text}*`; - } - - if (element.url) { - const linkText = element.children[0].text; - text = `[${linkText}](${element.url})`; - linkPreview = true; - } - - formattedText += text; - } + for (const element of richText.children) { + formattedText += applyFormatting(element); } formattedText += '\n'; } - formattedText = formattedText.replace(/\n$/, ''); + formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, ''); await instance.textMessage({ number: remoteJid.split('@')[0], options: { delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000, presence: 'composing', - linkPreview: linkPreview, }, textMessage: { text: formattedText, @@ -537,7 +530,6 @@ export class TypebotService { options: { delay: 1200, presence: 'composing', - linkPreview: false, }, textMessage: { text: formattedText, From 5f4a1b96ce898b1d7b6f7dd53488afc2d9f18769 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 18:08:30 -0300 Subject: [PATCH 11/36] fix: correction in typebot text formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b790e0..f5342f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Proxy configuration improvements * Correction in sending lists * Adjust in webhook_base64 +* Correction in typebot text formatting # 1.6.1 (2023-12-22 11:43) From 19fb9fcd314f59762d1be1653758ba4487e8e9a2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 18:20:38 -0300 Subject: [PATCH 12/36] fix: webhook --- src/whatsapp/controllers/instance.controller.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0701742c..0860f972 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -622,10 +622,15 @@ export class InstanceController { this.logger.verbose('deleting instance: ' + instanceName); - this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, { - instanceName, - instanceId: (await this.repository.auth.find(instanceName))?.instanceId, - }); + try { + this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, { + instanceName, + instanceId: (await this.repository.auth.find(instanceName))?.instanceId, + }); + } catch (error) { + this.logger.error(error); + } + delete this.waMonitor.waInstances[instanceName]; this.eventEmitter.emit('remove.instance', instanceName, 'inner'); return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; From 3ccb983377ad0d5fa94b7a0be5d953d73a714283 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 28 Dec 2023 18:24:29 -0300 Subject: [PATCH 13/36] fix: chatwoot service --- src/whatsapp/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 26b0cce9..deb9df20 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1029,7 +1029,7 @@ export class ChatwootService { .replaceAll(/(? Date: Fri, 29 Dec 2023 09:46:59 -0300 Subject: [PATCH 14/36] fix: exchange rabbitmq --- src/libs/amqp.server.ts | 5 ++++- src/whatsapp/services/whatsapp.service.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/amqp.server.ts b/src/libs/amqp.server.ts index fc95b33c..c861916b 100644 --- a/src/libs/amqp.server.ts +++ b/src/libs/amqp.server.ts @@ -27,6 +27,7 @@ export const initAMQP = () => { channel.assertExchange(exchangeName, 'topic', { durable: true, autoDelete: false, + assert: true, }); amqpChannel = channel; @@ -43,7 +44,7 @@ export const getAMQP = (): amqp.Channel | null => { }; export const initQueues = (instanceName: string, events: string[]) => { - if (!events || !events.length) return; + if (!instanceName || !events || !events.length) return; const queues = events.map((event) => { return `${event.replace(/_/g, '.').toLowerCase()}`; @@ -56,6 +57,7 @@ export const initQueues = (instanceName: string, events: string[]) => { amqp.assertExchange(exchangeName, 'topic', { durable: true, autoDelete: false, + assert: true, }); const queueName = `${instanceName}.${event}`; @@ -89,6 +91,7 @@ export const removeQueues = (instanceName: string, events: string[]) => { amqp.assertExchange(exchangeName, 'topic', { durable: true, autoDelete: false, + assert: true, }); const queueName = `${instanceName}.${event}`; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 7f247b0d..2f709e19 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -779,6 +779,7 @@ export class WAStartupService { amqp.assertExchange(exchangeName, 'topic', { durable: true, autoDelete: false, + assert: true, }); const queueName = `${this.instanceName}.${event}`; From d06ec604b9b55db6617264327e87d2cb45c055e0 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 29 Dec 2023 10:34:23 -0300 Subject: [PATCH 15/36] version: 1.6.2 --- Dockerfile | 2 +- package.json | 2 +- src/docs/swagger.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf9bccf4..9bc23317 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:20.7.0-alpine AS builder -LABEL version="1.6.1" description="Api to control whatsapp features through http requests." +LABEL version="1.6.2" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@agenciadgcode.com" diff --git a/package.json b/package.json index b8c413eb..5f68aa28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.6.1", + "version": "1.6.2", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 603aa0e0..924434e9 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -25,7 +25,7 @@ info: [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) - version: 1.6.1 + version: 1.6.2 contact: name: DavidsonGomes email: contato@agenciadgcode.com From fcd8815fca6a6950de1a3eb9c353cf688bb8a8e7 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Fri, 29 Dec 2023 14:52:16 -0300 Subject: [PATCH 16/36] fix(chatwoot): when possible use the original file extension In some cases mimeTypes.extension() return false to csv and other file types --- src/whatsapp/services/chatwoot.service.ts | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index d91b27ba..cb2faaf3 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1561,20 +1561,21 @@ export class ChatwootService { }, }); - let prependFilename: string; - if ( - body?.message[body?.messageType]?.fileName || - body?.message[body?.messageType]?.message?.documentMessage?.fileName - ) { - prependFilename = path.parse( - body?.message[body?.messageType]?.fileName || - body?.message[body?.messageType]?.message?.documentMessage?.fileName, - ).name; - prependFilename += `-${Math.floor(Math.random() * (99 - 10 + 1) + 10)}`; - } else { - prependFilename = Math.random().toString(36).substring(7); + let nameFile: string; + const messageBody = body?.message[body?.messageType]; + const originalFilename = messageBody?.fileName || messageBody?.message?.documentMessage?.fileName; + if (originalFilename) { + const parsedFile = path.parse(originalFilename); + if (parsedFile.name && parsedFile.ext) { + nameFile = `${parsedFile.name}-${Math.floor(Math.random() * (99 - 10 + 1) + 10)}${parsedFile.ext}`; + } + } + + if (!nameFile) { + nameFile = `${Math.random().toString(36).substring(7)}.${ + mimeTypes.extension(downloadBase64.mimetype) || '' + }`; } - const nameFile = `${prependFilename}.${mimeTypes.extension(downloadBase64.mimetype)}`; const fileData = Buffer.from(downloadBase64.base64, 'base64'); From c564ec41e25d97991d6ef2d56eac0d752c842690 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Fri, 29 Dec 2023 18:15:05 -0300 Subject: [PATCH 17/36] perf(chatwoot): Only use a axios request to get mimetype file if necessary --- src/whatsapp/services/chatwoot.service.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index cb2faaf3..1389d89a 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -916,17 +916,21 @@ export class ChatwootService { try { this.logger.verbose('get media type'); - const parts = media.split('/'); + const parsedMedia = path.parse(decodeURIComponent(media)); + let mimeType = mimeTypes.lookup(parsedMedia?.ext) || ''; + let fileName = parsedMedia?.name; - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); + if (!mimeType) { + const parts = media.split('/'); + fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); - const response = await axios.get(media, { - responseType: 'arraybuffer', - }); - - const mimeType = response.headers['content-type']; - this.logger.verbose('mime type: ' + mimeType); + const response = await axios.get(media, { + responseType: 'arraybuffer', + }); + mimeType = response.headers['content-type']; + this.logger.verbose('mime type: ' + mimeType); + } let type = 'document'; From d909550134d8686369315e61b161a1234d7f5fca Mon Sep 17 00:00:00 2001 From: jaison-x Date: Fri, 29 Dec 2023 18:42:07 -0300 Subject: [PATCH 18/36] fix(chatwoot): add file extension to var fileName --- src/whatsapp/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 1389d89a..851410be 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -918,7 +918,7 @@ export class ChatwootService { this.logger.verbose('get media type'); const parsedMedia = path.parse(decodeURIComponent(media)); let mimeType = mimeTypes.lookup(parsedMedia?.ext) || ''; - let fileName = parsedMedia?.name; + let fileName = parsedMedia?.name + parsedMedia?.ext; if (!mimeType) { const parts = media.split('/'); From 2dcd4d8fd3d8d5859068ec608d8cc46765fe035a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 29 Dec 2023 19:10:59 -0300 Subject: [PATCH 19/36] fix: Correction in chatwoot text formatting and render list message --- CHANGELOG.md | 1 + .../services/chatwoot.service copy.ts | 1968 +++++++++++++++++ src/whatsapp/services/chatwoot.service.ts | 104 +- src/whatsapp/services/typebot.service.ts | 5 +- 4 files changed, 2058 insertions(+), 20 deletions(-) create mode 100644 src/whatsapp/services/chatwoot.service copy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5342f38..4d0a58c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Correction in sending lists * Adjust in webhook_base64 * Correction in typebot text formatting +* Correction in chatwoot text formatting and render list message # 1.6.1 (2023-12-22 11:43) diff --git a/src/whatsapp/services/chatwoot.service copy.ts b/src/whatsapp/services/chatwoot.service copy.ts new file mode 100644 index 00000000..e58a2f25 --- /dev/null +++ b/src/whatsapp/services/chatwoot.service copy.ts @@ -0,0 +1,1968 @@ +import ChatwootClient from '@figuro/chatwoot-sdk'; +import axios from 'axios'; +import FormData from 'form-data'; +import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import Jimp from 'jimp'; +import mimeTypes from 'mime-types'; +import path from 'path'; + +import { ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { ROOT_DIR } from '../../config/path.config'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { MessageRaw } from '../models'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { Events } from '../types/wa.types'; +import { WAMonitoringService } from './monitor.service'; + +export class ChatwootService { + private messageCacheFile: string; + private messageCache: Set; + + private readonly logger = new Logger(ChatwootService.name); + + private provider: any; + + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + ) { + this.messageCache = new Set(); + } + + private loadMessageCache(): Set { + this.logger.verbose('load message cache'); + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); + } + } + + private saveMessageCache() { + this.logger.verbose('save message cache'); + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + this.logger.verbose('message cache saved'); + } + + private clearMessageCache() { + this.logger.verbose('clear message cache'); + this.messageCache.clear(); + this.saveMessageCache(); + } + + private async getProvider(instance: InstanceDto) { + this.logger.verbose('get provider to instance: ' + instance.instanceName); + const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); + + if (!provider) { + this.logger.warn('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + return provider; + // try { + // } catch (error) { + // this.logger.error('provider not found'); + // return null; + // } + } + + private async clientCw(instance: InstanceDto) { + this.logger.verbose('get client to instance: ' + instance.instanceName); + + const provider = await this.getProvider(instance); + + if (!provider) { + this.logger.error('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + this.provider = provider; + + this.logger.verbose('create client to instance: ' + instance.instanceName); + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + this.logger.verbose('client created'); + + return client; + } + + public async create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + + await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + this.logger.verbose('chatwoot created'); + + if (data.auto_create) { + const urlServer = this.configService.get('SERVER').URL; + + await this.initInstanceChatwoot( + instance, + instance.instanceName.split('-cwId-')[0], + `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, + true, + data.number, + ); + } + return data; + } + + public async find(instance: InstanceDto): Promise { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + try { + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + this.logger.error('chatwoot not found'); + return { enabled: null, url: '' }; + } + } + + public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose('get contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await client.contact.getContactable({ + accountId: this.provider.account_id, + id, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact found'); + return contact; + } + + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + number: string, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); + + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); + + let inboxId: number; + + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, + }; + + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; + } else { + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; + } + + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact( + instance, + '123456', + inboxId, + false, + 'EvolutionAPI', + 'https://evolution-api.com/files/evolution-api-favicon.png', + )) as any); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }; + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = 'init'; + + if (number) { + contentMsg = `init:${number}`; + } + + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: contentMsg, + message_type: 'outgoing', + }, + }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } + } + + this.logger.verbose('instance chatwoot initialized'); + return true; + } + + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + isGroup: boolean, + name?: string, + avatar_url?: string, + jid?: string, + ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let data: any = {}; + if (!isGroup) { + this.logger.verbose('create contact in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + identifier: jid, + avatar_url: avatar_url, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + avatar_url: avatar_url, + }; + } + + this.logger.verbose('create contact in chatwoot'); + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact created'); + return contact; + } + + public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose('update contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('update contact in chatwoot'); + try { + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); + + this.logger.verbose('contact updated'); + return contact; + } catch (error) { + this.logger.error(error); + } + } + + public async findContact(instance: InstanceDto, phoneNumber: string) { + this.logger.verbose('find contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let query: any; + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('format phone number'); + query = `+${phoneNumber}`; + } else { + this.logger.verbose('format group id'); + query = phoneNumber; + } + + this.logger.verbose('find contact in chatwoot'); + const contact: any = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('return contact'); + return contact.payload.find((contact) => contact.phone_number === query); + } else { + this.logger.verbose('return group'); + return contact.payload.find((contact) => contact.identifier === query); + } + } + + public async createConversation(instance: InstanceDto, body: any) { + this.logger.verbose('create conversation to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const isGroup = body.key.remoteJid.includes('@g.us'); + + this.logger.verbose('is group: ' + isGroup); + + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; + + this.logger.verbose('chat id: ' + chatId); + + let nameContact: string; + + nameContact = !body.key.fromMe ? body.pushName : chatId; + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + if (isGroup) { + this.logger.verbose('get group name'); + const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); + + nameContact = `${group.subject} (GROUP)`; + + this.logger.verbose('find or create participant in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + body.key.participant.split('@')[0], + ); + + const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); + + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } + } else { + await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + body.key.participant, + ); + } + } + + this.logger.verbose('find or create contact in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); + + const findContact = await this.findContact(instance, chatId); + + let contact: any; + if (body.key.fromMe) { + if (findContact) { + contact = await this.updateContact(instance, findContact.id, { + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + const jid = isGroup ? null : body.key.remoteJid; + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + jid, + ); + } + } else { + if (findContact) { + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = await this.updateContact(instance, findContact.id, { + avatar_url: picture_url.profilePictureUrl || null, + }); + } + if (!contact) { + contact = await this.findContact(instance, chatId); + } + } else { + const jid = isGroup ? null : body.key.remoteJid; + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + jid, + ); + } + } + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } + + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + let conversation: any; + if (this.provider.reopen_conversation) { + conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); + + if (this.provider.conversation_pending) { + await client.conversations.toggleStatus({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + status: 'pending', + }, + }); + } + } else { + conversation = contactConversations.payload.find( + (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + } + this.logger.verbose('return conversation if exists'); + + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } + + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find inbox by name'); + const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName.split('-cwId-')[0]); + + if (!findByName) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('return inbox'); + return findByName; + } + + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + messageBody?: any, + ) { + this.logger.verbose('create message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const replyToIds = await this.getReplyToIds(messageBody, instance); + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + content_attributes: { + ...replyToIds, + }, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('message created'); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create bot message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + instance?: InstanceDto, + messageBody?: any, + ) { + this.logger.verbose('send data to chatwoot'); + + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + + if (messageBody && instance) { + const replyToIds = await this.getReplyToIds(messageBody, instance); + + if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { + data.append('content_attributes', { + ...replyToIds, + }); + } + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + unlinkSync(file); + } + } + + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + this.logger.verbose('create bot qr to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('send data to chatwoot'); + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + if (file) { + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + } + } + + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { + this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + + try { + this.logger.verbose('get media type'); + const parts = media.split('/'); + + const fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); + + const response = await axios.get(media, { + responseType: 'arraybuffer', + }); + + const mimeType = response.headers['content-type']; + this.logger.verbose('mime type: ' + mimeType); + + let type = 'document'; + + switch (mimeType.split('/')[0]) { + case 'image': + type = 'image'; + break; + case 'video': + type = 'video'; + break; + case 'audio': + type = 'audio'; + break; + default: + type = 'document'; + break; + } + + this.logger.verbose('type: ' + type); + + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + ...options, + }, + }; + + const messageSent = await waInstance?.audioWhatsapp(data, true); + + this.logger.verbose('audio sent'); + return messageSent; + } + + this.logger.verbose('send media to instance: ' + waInstance.instanceName); + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + ...options, + }, + }; + + if (caption) { + this.logger.verbose('caption found'); + data.mediaMessage.caption = caption; + } + + const messageSent = await waInstance?.mediaMessage(data, true); + + this.logger.verbose('media sent'); + return messageSent; + } catch (error) { + this.logger.error(error); + } + } + + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + await new Promise((resolve) => setTimeout(resolve, 500)); + + this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('check if is bot'); + if ( + !body?.conversation || + body.private || + (body.event === 'message_updated' && !body.content_attributes?.deleted) + ) { + return { message: 'bot' }; + } + + this.logger.verbose('check if is group'); + const chatId = + body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier; + // Chatwoot to Whatsapp + const messageReceived = body.content + ? body.content + .replaceAll(/(? 0) { + this.logger.verbose('message is media'); + for (const attachment of message.attachments) { + this.logger.verbose('send media to whatsapp'); + if (!messageReceived) { + this.logger.verbose('message do not have text'); + formatText = null; + } + + const options: Options = { + quoted: await this.getQuotedMessage(body, instance), + }; + + const messageSent = await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + options, + ); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + { + messageId: body.id, + inboxId: body.inbox?.id, + conversationId: body.conversation?.id, + }, + instance, + ); + } + } else { + this.logger.verbose('message is text'); + + this.logger.verbose('send text to whatsapp'); + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + quoted: await this.getQuotedMessage(body, instance), + }, + }; + + const messageSent = await waInstance?.textMessage(data, true); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + { + messageId: body.id, + inboxId: body.inbox?.id, + conversationId: body.conversation?.id, + }, + instance, + ); + } + } + } + + if (body.message_type === 'template' && body.event === 'message_created') { + this.logger.verbose('check if is template'); + + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + this.logger.verbose('send text to whatsapp'); + + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; + } + } + + private updateChatwootMessageId( + message: MessageRaw, + chatwootMessageIds: MessageRaw['chatwoot'], + instance: InstanceDto, + ) { + if (!chatwootMessageIds.messageId || !message?.key?.id) { + return; + } + + message.chatwoot = chatwootMessageIds; + this.repository.message.update([message], instance.instanceName, true); + } + + private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { + const messages = await this.repository.message.find({ + where: { + key: { + id: keyId, + }, + owner: instance.instanceName, + }, + limit: 1, + }); + + return messages.length ? messages[0] : null; + } + + private async getReplyToIds( + msg: any, + instance: InstanceDto, + ): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> { + let inReplyTo = null; + let inReplyToExternalId = null; + + if (msg) { + inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId; + if (inReplyToExternalId) { + const message = await this.getMessageByKeyId(instance, inReplyToExternalId); + if (message?.chatwoot?.messageId) { + inReplyTo = message.chatwoot.messageId; + } + } + } + + return { + in_reply_to: inReplyTo, + in_reply_to_external_id: inReplyToExternalId, + }; + } + + private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { + if (msg?.content_attributes?.in_reply_to) { + const message = await this.repository.message.find({ + where: { + chatwoot: { + messageId: msg?.content_attributes?.in_reply_to, + }, + owner: instance.instanceName, + }, + limit: 1, + }); + if (message.length && message[0]?.key?.id) { + return { + key: message[0].key, + message: message[0].message, + }; + } + } + + return null; + } + + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + + const result = messageKeys.some((key) => media.includes(key)); + + this.logger.verbose('is media message: ' + result); + return result; + } + + private getAdsMessage(msg: any) { + interface AdsMessage { + title: string; + body: string; + thumbnailUrl: string; + sourceUrl: string; + } + const adsMessage: AdsMessage | undefined = msg.extendedTextMessage?.contextInfo?.externalAdReply; + + this.logger.verbose('Get ads message if it exist'); + adsMessage && this.logger.verbose('Ads message: ' + adsMessage); + return adsMessage; + } + + private getReactionMessage(msg: any) { + interface ReactionMessage { + key: MessageRaw['key']; + text: string; + } + const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage; + + this.logger.verbose('Get reaction message if it exists'); + reactionMessage && this.logger.verbose('Reaction message: ' + reactionMessage); + return reactionMessage; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: undefined, + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, + // Alterado por Edison Martins em 29/12/2023 + // Inclusão do tipo de mensagem de Lista + listMessage: msg.listMessage, + listResponseMessage: msg.listResponseMessage, + }; + + this.logger.verbose('type message: ' + types); + + return types; + } + + private getMessageContent(types: any) { + this.logger.verbose('get message content'); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + const result = typeKey ? types[typeKey] : undefined; + + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { + const latitude = result.degreesLatitude; + const longitude = result.degreesLongitude; + // Alterado por Edison Martins em 29/12/2023 + // Ajuste no formato da mensagem de Localização + const locationName = result?.name || 'Não informado'; + const locationAddress = result?.address || 'Não informado'; + + const formattedLocation = + '*Localização:*\n\n' + + '_Latitude:_ ' + + latitude + + '\n' + + '_Longitude:_ ' + + longitude + + '\n' + + '_Nome:_ ' + + locationName + + '\n' + + '_Endereço:_ ' + + locationAddress + + '\n' + + '_Url:_ https://www.google.com/maps/search/?api=1&query=' + + latitude + + ',' + + longitude; + + this.logger.verbose('message content: ' + formattedLocation); + + return formattedLocation; + } + + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + // Alterado por Edison Martins em 29/12/2023 + // Ajuste no formato da mensagem de Contato + let formattedContact = '*Contato:*\n\n' + '_Nome:_ ' + contactInfo['FN']; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += '\n_Número (' + numberCount + '):_ ' + phoneNumber; + numberCount++; + } + }); + + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } + + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = '*Contato:*\n\n' + '_Nome:_ ' + contact.displayName; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += '\n_Número (' + numberCount + '):_ ' + phoneNumber; + numberCount++; + } + }); + + return formattedContact; + }); + + const formattedContactsArray = formattedContacts.join('\n\n'); + + this.logger.verbose('formatted contacts: ' + formattedContactsArray); + + return formattedContactsArray; + } + + // Alterado por Edison Martins em 29/12/2023 + // Inclusão do tipo de mensagem de Lista + if (typeKey === 'listMessage') { + const listTitle = result?.title || 'Não informado'; + const listDescription = result?.description || 'Não informado'; + const listFooter = result?.footerText || 'Não informado'; + + let formattedList = + '*Menu em Lista:*\n\n' + + '_Título_: ' + + listTitle + + '\n' + + '_Descrição_: ' + + listDescription + + '\n' + + '_Rodapé_: ' + + listFooter; + + if (result.sections && result.sections.length > 0) { + result.sections.forEach((section, sectionIndex) => { + formattedList += '\n\n*Seção ' + (sectionIndex + 1) + ':* ' + section.title || 'Não informado\n'; + + if (section.rows && section.rows.length > 0) { + section.rows.forEach((row, rowIndex) => { + formattedList += '\n*Linha ' + (rowIndex + 1) + ':*\n'; + formattedList += '_▪️ Título:_ ' + (row.title || 'Não informado') + '\n'; + formattedList += '_▪️ Descrição:_ ' + (row.description || 'Não informado') + '\n'; + formattedList += '_▪️ ID:_ ' + (row.rowId || 'Não informado') + '\n'; + }); + } else { + formattedList += '\nNenhuma linha encontrada nesta seção.\n'; + } + }); + } else { + formattedList += '\nNenhuma seção encontrada.\n'; + } + + return formattedList; + } + + if (typeKey === 'listResponseMessage') { + const responseTitle = result?.title || 'Não informado'; + const responseDescription = result?.description || 'Não informado'; + const responseRowId = result?.singleSelectReply?.selectedRowId || 'Não informado'; + + const formattedResponseList = + '*Resposta da Lista:*\n\n' + + '_Título_: ' + + responseTitle + + '\n' + + '_Descrição_: ' + + responseDescription + + '\n' + + '_ID_: ' + + responseRowId; + return formattedResponseList; + } + + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { + this.logger.verbose('get conversation message'); + + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + this.logger.verbose('conversation message: ' + messageContent); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (event === 'messages.upsert' || event === 'send.message') { + this.logger.verbose('event messages.upsert'); + + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + + this.logger.verbose('get conversation message'); + + // Whatsapp to Chatwoot + const originalMessage = await this.getConversationMessage(body.message); + const bodyMessage = originalMessage + ? originalMessage + .replaceAll(/\*((?!\s)([^\n*]+?)(? { + await img.cover(320, 180).writeAsync(fileName); + }) + .catch((err) => { + this.logger.error(`image is not write: ${err}`); + }); + const truncStr = (str: string, len: number) => { + return str.length > len ? str.substring(0, len) + '...' : str; + }; + + const title = truncStr(adsMessage.title, 40); + const description = truncStr(adsMessage.body, 75); + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData( + getConversation, + fileName, + messageType, + `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, + instance, + body, + ); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + if (event === Events.MESSAGES_DELETE) { + this.logger.verbose('deleting message from instance: ' + instance.instanceName); + + if (!body?.key?.id) { + this.logger.warn('message id not found'); + return; + } + + const message = await this.getMessageByKeyId(instance, body.key.id); + if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { + this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); + return await client.messages.delete({ + accountId: this.provider.account_id, + conversationId: message.chatwoot.conversationId, + messageId: message.chatwoot.messageId, + }); + } + } + + if (event === 'status.instance') { + this.logger.verbose('event status.instance'); + const data = body; + const inbox = await this.getInbox(instance); + + if (!inbox) { + this.logger.warn('inbox not found'); + return; + } + + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); + + if (body.status === 'open') { + // if we have qrcode count then we understand that a new connection was established + if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { + const msgConnection = `🚀 Connection successfully established!`; + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + } + + if (event === 'qrcode.updated') { + this.logger.verbose('event qrcode.updated'); + if (body.statusCode === 500) { + this.logger.verbose('qrcode error'); + const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.`; + + this.logger.verbose('send message to chatwoot'); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + this.logger.verbose('qrcode success'); + const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; + + this.logger.verbose('temp file name: ' + fileName); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('send qrcode to chatwoot'); + await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); + + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( + 4, + 8, + )}`; + } + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); + } + } +} diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 998d244f..f5ff082b 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1332,6 +1332,8 @@ export class ChatwootService { contactsArrayMessage: msg.contactsArrayMessage, locationMessage: msg.locationMessage, liveLocationMessage: msg.liveLocationMessage, + listMessage: msg.listMessage, + listResponseMessage: msg.listResponseMessage, }; this.logger.verbose('type message: ' + types); @@ -1349,11 +1351,27 @@ export class ChatwootService { const latitude = result.degreesLatitude; const longitude = result.degreesLongitude; - const formattedLocation = `**Location:** - **latitude:** ${latitude} - **longitude:** ${longitude} - https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} - `; + const locationName = result?.name || 'Unknown'; + const locationAddress = result?.address || 'Unknown'; + + const formattedLocation = + '*Localização:*\n\n' + + '_Latitude:_ ' + + latitude + + '\n' + + '_Longitude:_ ' + + longitude + + '\n' + + '_Nome:_ ' + + locationName + + '\n' + + '_Endereço:_ ' + + locationAddress + + '\n' + + '_Url:_ https://www.google.com/maps/search/?api=1&query=' + + latitude + + ',' + + longitude; this.logger.verbose('message content: ' + formattedLocation); @@ -1371,19 +1389,17 @@ export class ChatwootService { } }); - let formattedContact = `**Contact:** - **name:** ${contactInfo['FN']}`; + let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contactInfo['FN']; let numberCount = 1; Object.keys(contactInfo).forEach((key) => { if (key.startsWith('item') && key.includes('TEL')) { const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber; numberCount++; - } - if (key.includes('TEL')) { + } else if (key.includes('TEL')) { const phoneNumber = contactInfo[key]; - formattedContact += `\n**number:** ${phoneNumber}`; + formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber; numberCount++; } }); @@ -1404,19 +1420,17 @@ export class ChatwootService { } }); - let formattedContact = `**Contact:** - **name:** ${contact.displayName}`; + let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contact.displayName; let numberCount = 1; Object.keys(contactInfo).forEach((key) => { if (key.startsWith('item') && key.includes('TEL')) { const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber; numberCount++; - } - if (key.includes('TEL')) { + } else if (key.includes('TEL')) { const phoneNumber = contactInfo[key]; - formattedContact += `\n**number:** ${phoneNumber}`; + formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber; numberCount++; } }); @@ -1431,6 +1445,62 @@ export class ChatwootService { return formattedContactsArray; } + if (typeKey === 'listMessage') { + const listTitle = result?.title || 'Unknown'; + const listDescription = result?.description || 'Unknown'; + const listFooter = result?.footerText || 'Unknown'; + + let formattedList = + '*List Menu:*\n\n' + + '_Title_: ' + + listTitle + + '\n' + + '_Description_: ' + + listDescription + + '\n' + + '_Footer_: ' + + listFooter; + + if (result.sections && result.sections.length > 0) { + result.sections.forEach((section, sectionIndex) => { + formattedList += '\n\n*Section ' + (sectionIndex + 1) + ':* ' + section.title || 'Unknown\n'; + + if (section.rows && section.rows.length > 0) { + section.rows.forEach((row, rowIndex) => { + formattedList += '\n*Line ' + (rowIndex + 1) + ':*\n'; + formattedList += '_▪️ Title:_ ' + (row.title || 'Unknown') + '\n'; + formattedList += '_▪️ Description:_ ' + (row.description || 'Unknown') + '\n'; + formattedList += '_▪️ ID:_ ' + (row.rowId || 'Unknown') + '\n'; + }); + } else { + formattedList += '\nNo lines found in this section.\n'; + } + }); + } else { + formattedList += '\nNo sections found.\n'; + } + + return formattedList; + } + + if (typeKey === 'listResponseMessage') { + const responseTitle = result?.title || 'Unknown'; + const responseDescription = result?.description || 'Unknown'; + const responseRowId = result?.singleSelectReply?.selectedRowId || 'Unknown'; + + const formattedResponseList = + '*List Response:*\n\n' + + '_Title_: ' + + responseTitle + + '\n' + + '_Description_: ' + + responseDescription + + '\n' + + '_ID_: ' + + responseRowId; + return formattedResponseList; + } + this.logger.verbose('message content: ' + result); return result; diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index e4e36310..b5640240 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -412,7 +412,7 @@ export class TypebotService { text += element.text; } - if (element.type === 'p' || element.type === 'inline-variable') { + if (element.type === 'p' || element.type === 'inline-variable' || element.type === 'a') { for (const child of element.children) { text += applyFormatting(child); } @@ -435,8 +435,7 @@ export class TypebotService { let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`; if (element.url) { - const linkText = element.children[0]?.text || ''; - formattedText = `[${linkText}](${element.url})`; + formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`; } return formattedText; From 85d182523634cba66cad9e42b3b65be32ca7abe4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 29 Dec 2023 19:12:29 -0300 Subject: [PATCH 20/36] version: 1.6.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0a58c2..eff9da7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ * Adjust in webhook_base64 * Correction in typebot text formatting * Correction in chatwoot text formatting and render list message +* Only use a axios request to get file mimetype if necessary +* When possible use the original file extension +* When receiving a file from whatsapp, use the original filename in chatwoot if possible +* Remove message ids cache in chatwoot to use chatwoot's api itself # 1.6.1 (2023-12-22 11:43) From dfceda394268e8bd0ecf75ebc176d56a80307fc9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 29 Dec 2023 19:15:51 -0300 Subject: [PATCH 21/36] version: 1.6.2 --- .../services/chatwoot.service copy.ts | 1968 ----------------- 1 file changed, 1968 deletions(-) delete mode 100644 src/whatsapp/services/chatwoot.service copy.ts diff --git a/src/whatsapp/services/chatwoot.service copy.ts b/src/whatsapp/services/chatwoot.service copy.ts deleted file mode 100644 index e58a2f25..00000000 --- a/src/whatsapp/services/chatwoot.service copy.ts +++ /dev/null @@ -1,1968 +0,0 @@ -import ChatwootClient from '@figuro/chatwoot-sdk'; -import axios from 'axios'; -import FormData from 'form-data'; -import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; -import Jimp from 'jimp'; -import mimeTypes from 'mime-types'; -import path from 'path'; - -import { ConfigService, HttpServer } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; -import { ROOT_DIR } from '../../config/path.config'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { InstanceDto } from '../dto/instance.dto'; -import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; -import { MessageRaw } from '../models'; -import { RepositoryBroker } from '../repository/repository.manager'; -import { Events } from '../types/wa.types'; -import { WAMonitoringService } from './monitor.service'; - -export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; - - private readonly logger = new Logger(ChatwootService.name); - - private provider: any; - - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - ) { - this.messageCache = new Set(); - } - - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); - } - } - - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); - } - - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); - } - - private async getProvider(instance: InstanceDto) { - this.logger.verbose('get provider to instance: ' + instance.instanceName); - const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); - - if (!provider) { - this.logger.warn('provider not found'); - return null; - } - - this.logger.verbose('provider found'); - - return provider; - // try { - // } catch (error) { - // this.logger.error('provider not found'); - // return null; - // } - } - - private async clientCw(instance: InstanceDto) { - this.logger.verbose('get client to instance: ' + instance.instanceName); - - const provider = await this.getProvider(instance); - - if (!provider) { - this.logger.error('provider not found'); - return null; - } - - this.logger.verbose('provider found'); - - this.provider = provider; - - this.logger.verbose('create client to instance: ' + instance.instanceName); - const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, - }); - - this.logger.verbose('client created'); - - return client; - } - - public async create(instance: InstanceDto, data: ChatwootDto) { - this.logger.verbose('create chatwoot: ' + instance.instanceName); - - await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); - - this.logger.verbose('chatwoot created'); - - if (data.auto_create) { - const urlServer = this.configService.get('SERVER').URL; - - await this.initInstanceChatwoot( - instance, - instance.instanceName.split('-cwId-')[0], - `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, - true, - data.number, - ); - } - return data; - } - - public async find(instance: InstanceDto): Promise { - this.logger.verbose('find chatwoot: ' + instance.instanceName); - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch (error) { - this.logger.error('chatwoot not found'); - return { enabled: null, url: '' }; - } - } - - public async getContact(instance: InstanceDto, id: number) { - this.logger.verbose('get contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await client.contact.getContactable({ - accountId: this.provider.account_id, - id, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact found'); - return contact; - } - - public async initInstanceChatwoot( - instance: InstanceDto, - inboxName: string, - webhookUrl: string, - qrcode: boolean, - number: string, - ) { - this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inbox in chatwoot'); - const findInbox: any = await client.inboxes.list({ - accountId: this.provider.account_id, - }); - - this.logger.verbose('check duplicate inbox'); - const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); - - let inboxId: number; - - if (!checkDuplicate) { - this.logger.verbose('create inbox in chatwoot'); - const data = { - type: 'api', - webhook_url: webhookUrl, - }; - - const inbox = await client.inboxes.create({ - accountId: this.provider.account_id, - data: { - name: inboxName, - channel: data as any, - }, - }); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } else { - this.logger.verbose('find inbox in chatwoot'); - const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } - - this.logger.verbose('find contact in chatwoot and create if not exists'); - const contact = - (await this.findContact(instance, '123456')) || - ((await this.createContact( - instance, - '123456', - inboxId, - false, - 'EvolutionAPI', - 'https://evolution-api.com/files/evolution-api-favicon.png', - )) as any); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact.id || contact.payload.contact.id; - - if (qrcode) { - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }; - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('create message for init instance in chatwoot'); - - let contentMsg = 'init'; - - if (number) { - contentMsg = `init:${number}`; - } - - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: contentMsg, - message_type: 'outgoing', - }, - }); - - if (!message) { - this.logger.warn('conversation not found'); - return null; - } - } - - this.logger.verbose('instance chatwoot initialized'); - return true; - } - - public async createContact( - instance: InstanceDto, - phoneNumber: string, - inboxId: number, - isGroup: boolean, - name?: string, - avatar_url?: string, - jid?: string, - ) { - this.logger.verbose('create contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let data: any = {}; - if (!isGroup) { - this.logger.verbose('create contact in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - phone_number: `+${phoneNumber}`, - identifier: jid, - avatar_url: avatar_url, - }; - } else { - this.logger.verbose('create contact group in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: phoneNumber, - avatar_url: avatar_url, - }; - } - - this.logger.verbose('create contact in chatwoot'); - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact created'); - return contact; - } - - public async updateContact(instance: InstanceDto, id: number, data: any) { - this.logger.verbose('update contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('update contact in chatwoot'); - try { - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); - - this.logger.verbose('contact updated'); - return contact; - } catch (error) { - this.logger.error(error); - } - } - - public async findContact(instance: InstanceDto, phoneNumber: string) { - this.logger.verbose('find contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let query: any; - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('format phone number'); - query = `+${phoneNumber}`; - } else { - this.logger.verbose('format group id'); - query = phoneNumber; - } - - this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); - } else { - this.logger.verbose('return group'); - return contact.payload.find((contact) => contact.identifier === query); - } - } - - public async createConversation(instance: InstanceDto, body: any) { - this.logger.verbose('create conversation to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const isGroup = body.key.remoteJid.includes('@g.us'); - - this.logger.verbose('is group: ' + isGroup); - - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - - this.logger.verbose('chat id: ' + chatId); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - if (isGroup) { - this.logger.verbose('get group name'); - const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); - - nameContact = `${group.subject} (GROUP)`; - - this.logger.verbose('find or create participant in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( - body.key.participant.split('@')[0], - ); - - const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); - - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - body.key.participant, - ); - } - } - - this.logger.verbose('find or create contact in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); - - const findContact = await this.findContact(instance, chatId); - - let contact: any; - if (body.key.fromMe) { - if (findContact) { - contact = await this.updateContact(instance, findContact.id, { - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - const jid = isGroup ? null : body.key.remoteJid; - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - jid, - ); - } - } else { - if (findContact) { - if (!findContact.name || findContact.name === chatId) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - contact = await this.updateContact(instance, findContact.id, { - avatar_url: picture_url.profilePictureUrl || null, - }); - } - if (!contact) { - contact = await this.findContact(instance, chatId); - } - } else { - const jid = isGroup ? null : body.key.remoteJid; - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - jid, - ); - } - } - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - this.logger.verbose('update contact name in chatwoot'); - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } - - this.logger.verbose('get contact conversations in chatwoot'); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; - - if (contactConversations) { - let conversation: any; - if (this.provider.reopen_conversation) { - conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); - - if (this.provider.conversation_pending) { - await client.conversations.toggleStatus({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - status: 'pending', - }, - }); - } - } else { - conversation = contactConversations.payload.find( - (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - } - this.logger.verbose('return conversation if exists'); - - if (conversation) { - this.logger.verbose('conversation found'); - return conversation.id; - } - } - - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: filterInbox.id.toString(), - }; - - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('conversation created'); - return conversation.id; - } catch (error) { - this.logger.error(error); - } - } - - public async getInbox(instance: InstanceDto) { - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inboxes in chatwoot'); - const inbox = (await client.inboxes.list({ - accountId: this.provider.account_id, - })) as any; - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName.split('-cwId-')[0]); - - if (!findByName) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('return inbox'); - return findByName; - } - - public async createMessage( - instance: InstanceDto, - conversationId: number, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - messageBody?: any, - ) { - this.logger.verbose('create message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const replyToIds = await this.getReplyToIds(messageBody, instance); - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - content_attributes: { - ...replyToIds, - }, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('message created'); - - return message; - } - - public async createBotMessage( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create bot message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: content, - message_type: messageType, - attachments: attachments, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('bot message created'); - - return message; - } - - private async sendData( - conversationId: number, - file: string, - messageType: 'incoming' | 'outgoing' | undefined, - content?: string, - instance?: InstanceDto, - messageBody?: any, - ) { - this.logger.verbose('send data to chatwoot'); - - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - - if (messageBody && instance) { - const replyToIds = await this.getReplyToIds(messageBody, instance); - - if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { - data.append('content_attributes', { - ...replyToIds, - }); - } - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - unlinkSync(file); - } - } - - public async createBotQr( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - file?: string, - ) { - this.logger.verbose('create bot qr to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('send data to chatwoot'); - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - if (file) { - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - } - } - - public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { - this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); - - try { - this.logger.verbose('get media type'); - const parts = media.split('/'); - - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); - - const response = await axios.get(media, { - responseType: 'arraybuffer', - }); - - const mimeType = response.headers['content-type']; - this.logger.verbose('mime type: ' + mimeType); - - let type = 'document'; - - switch (mimeType.split('/')[0]) { - case 'image': - type = 'image'; - break; - case 'video': - type = 'video'; - break; - case 'audio': - type = 'audio'; - break; - default: - type = 'document'; - break; - } - - this.logger.verbose('type: ' + type); - - if (type === 'audio') { - this.logger.verbose('send audio to instance: ' + waInstance.instanceName); - const data: SendAudioDto = { - number: number, - audioMessage: { - audio: media, - }, - options: { - delay: 1200, - presence: 'recording', - ...options, - }, - }; - - const messageSent = await waInstance?.audioWhatsapp(data, true); - - this.logger.verbose('audio sent'); - return messageSent; - } - - this.logger.verbose('send media to instance: ' + waInstance.instanceName); - const data: SendMediaDto = { - number: number, - mediaMessage: { - mediatype: type as any, - fileName: fileName, - media: media, - }, - options: { - delay: 1200, - presence: 'composing', - ...options, - }, - }; - - if (caption) { - this.logger.verbose('caption found'); - data.mediaMessage.caption = caption; - } - - const messageSent = await waInstance?.mediaMessage(data, true); - - this.logger.verbose('media sent'); - return messageSent; - } catch (error) { - this.logger.error(error); - } - } - - public async receiveWebhook(instance: InstanceDto, body: any) { - try { - await new Promise((resolve) => setTimeout(resolve, 500)); - - this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('check if is bot'); - if ( - !body?.conversation || - body.private || - (body.event === 'message_updated' && !body.content_attributes?.deleted) - ) { - return { message: 'bot' }; - } - - this.logger.verbose('check if is group'); - const chatId = - body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier; - // Chatwoot to Whatsapp - const messageReceived = body.content - ? body.content - .replaceAll(/(? 0) { - this.logger.verbose('message is media'); - for (const attachment of message.attachments) { - this.logger.verbose('send media to whatsapp'); - if (!messageReceived) { - this.logger.verbose('message do not have text'); - formatText = null; - } - - const options: Options = { - quoted: await this.getQuotedMessage(body, instance), - }; - - const messageSent = await this.sendAttachment( - waInstance, - chatId, - attachment.data_url, - formatText, - options, - ); - - this.updateChatwootMessageId( - { - ...messageSent, - owner: instance.instanceName, - }, - { - messageId: body.id, - inboxId: body.inbox?.id, - conversationId: body.conversation?.id, - }, - instance, - ); - } - } else { - this.logger.verbose('message is text'); - - this.logger.verbose('send text to whatsapp'); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: formatText, - }, - options: { - delay: 1200, - presence: 'composing', - quoted: await this.getQuotedMessage(body, instance), - }, - }; - - const messageSent = await waInstance?.textMessage(data, true); - - this.updateChatwootMessageId( - { - ...messageSent, - owner: instance.instanceName, - }, - { - messageId: body.id, - inboxId: body.inbox?.id, - conversationId: body.conversation?.id, - }, - instance, - ); - } - } - } - - if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is template'); - - const data: SendTextDto = { - number: chatId, - textMessage: { - text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - this.logger.verbose('send text to whatsapp'); - - await waInstance?.textMessage(data); - } - - return { message: 'bot' }; - } catch (error) { - this.logger.error(error); - - return { message: 'bot' }; - } - } - - private updateChatwootMessageId( - message: MessageRaw, - chatwootMessageIds: MessageRaw['chatwoot'], - instance: InstanceDto, - ) { - if (!chatwootMessageIds.messageId || !message?.key?.id) { - return; - } - - message.chatwoot = chatwootMessageIds; - this.repository.message.update([message], instance.instanceName, true); - } - - private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { - const messages = await this.repository.message.find({ - where: { - key: { - id: keyId, - }, - owner: instance.instanceName, - }, - limit: 1, - }); - - return messages.length ? messages[0] : null; - } - - private async getReplyToIds( - msg: any, - instance: InstanceDto, - ): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> { - let inReplyTo = null; - let inReplyToExternalId = null; - - if (msg) { - inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId; - if (inReplyToExternalId) { - const message = await this.getMessageByKeyId(instance, inReplyToExternalId); - if (message?.chatwoot?.messageId) { - inReplyTo = message.chatwoot.messageId; - } - } - } - - return { - in_reply_to: inReplyTo, - in_reply_to_external_id: inReplyToExternalId, - }; - } - - private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { - if (msg?.content_attributes?.in_reply_to) { - const message = await this.repository.message.find({ - where: { - chatwoot: { - messageId: msg?.content_attributes?.in_reply_to, - }, - owner: instance.instanceName, - }, - limit: 1, - }); - if (message.length && message[0]?.key?.id) { - return { - key: message[0].key, - message: message[0].message, - }; - } - } - - return null; - } - - private isMediaMessage(message: any) { - this.logger.verbose('check if is media message'); - const media = [ - 'imageMessage', - 'documentMessage', - 'documentWithCaptionMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - ]; - - const messageKeys = Object.keys(message); - - const result = messageKeys.some((key) => media.includes(key)); - - this.logger.verbose('is media message: ' + result); - return result; - } - - private getAdsMessage(msg: any) { - interface AdsMessage { - title: string; - body: string; - thumbnailUrl: string; - sourceUrl: string; - } - const adsMessage: AdsMessage | undefined = msg.extendedTextMessage?.contextInfo?.externalAdReply; - - this.logger.verbose('Get ads message if it exist'); - adsMessage && this.logger.verbose('Ads message: ' + adsMessage); - return adsMessage; - } - - private getReactionMessage(msg: any) { - interface ReactionMessage { - key: MessageRaw['key']; - text: string; - } - const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage; - - this.logger.verbose('Get reaction message if it exists'); - reactionMessage && this.logger.verbose('Reaction message: ' + reactionMessage); - return reactionMessage; - } - - private getTypeMessage(msg: any) { - this.logger.verbose('get type message'); - - const types = { - conversation: msg.conversation, - imageMessage: msg.imageMessage?.caption, - videoMessage: msg.videoMessage?.caption, - extendedTextMessage: msg.extendedTextMessage?.text, - messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: undefined, - documentMessage: msg.documentMessage?.caption, - documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, - audioMessage: msg.audioMessage?.caption, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: msg.locationMessage, - liveLocationMessage: msg.liveLocationMessage, - // Alterado por Edison Martins em 29/12/2023 - // Inclusão do tipo de mensagem de Lista - listMessage: msg.listMessage, - listResponseMessage: msg.listResponseMessage, - }; - - this.logger.verbose('type message: ' + types); - - return types; - } - - private getMessageContent(types: any) { - this.logger.verbose('get message content'); - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); - - const result = typeKey ? types[typeKey] : undefined; - - if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { - const latitude = result.degreesLatitude; - const longitude = result.degreesLongitude; - // Alterado por Edison Martins em 29/12/2023 - // Ajuste no formato da mensagem de Localização - const locationName = result?.name || 'Não informado'; - const locationAddress = result?.address || 'Não informado'; - - const formattedLocation = - '*Localização:*\n\n' + - '_Latitude:_ ' + - latitude + - '\n' + - '_Longitude:_ ' + - longitude + - '\n' + - '_Nome:_ ' + - locationName + - '\n' + - '_Endereço:_ ' + - locationAddress + - '\n' + - '_Url:_ https://www.google.com/maps/search/?api=1&query=' + - latitude + - ',' + - longitude; - - this.logger.verbose('message content: ' + formattedLocation); - - return formattedLocation; - } - - if (typeKey === 'contactMessage') { - const vCardData = result.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - // Alterado por Edison Martins em 29/12/2023 - // Ajuste no formato da mensagem de Contato - let formattedContact = '*Contato:*\n\n' + '_Nome:_ ' + contactInfo['FN']; - - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += '\n_Número (' + numberCount + '):_ ' + phoneNumber; - numberCount++; - } - }); - - this.logger.verbose('message content: ' + formattedContact); - return formattedContact; - } - - if (typeKey === 'contactsArrayMessage') { - const formattedContacts = result.contacts.map((contact) => { - const vCardData = contact.vcard.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = '*Contato:*\n\n' + '_Nome:_ ' + contact.displayName; - - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += '\n_Número (' + numberCount + '):_ ' + phoneNumber; - numberCount++; - } - }); - - return formattedContact; - }); - - const formattedContactsArray = formattedContacts.join('\n\n'); - - this.logger.verbose('formatted contacts: ' + formattedContactsArray); - - return formattedContactsArray; - } - - // Alterado por Edison Martins em 29/12/2023 - // Inclusão do tipo de mensagem de Lista - if (typeKey === 'listMessage') { - const listTitle = result?.title || 'Não informado'; - const listDescription = result?.description || 'Não informado'; - const listFooter = result?.footerText || 'Não informado'; - - let formattedList = - '*Menu em Lista:*\n\n' + - '_Título_: ' + - listTitle + - '\n' + - '_Descrição_: ' + - listDescription + - '\n' + - '_Rodapé_: ' + - listFooter; - - if (result.sections && result.sections.length > 0) { - result.sections.forEach((section, sectionIndex) => { - formattedList += '\n\n*Seção ' + (sectionIndex + 1) + ':* ' + section.title || 'Não informado\n'; - - if (section.rows && section.rows.length > 0) { - section.rows.forEach((row, rowIndex) => { - formattedList += '\n*Linha ' + (rowIndex + 1) + ':*\n'; - formattedList += '_▪️ Título:_ ' + (row.title || 'Não informado') + '\n'; - formattedList += '_▪️ Descrição:_ ' + (row.description || 'Não informado') + '\n'; - formattedList += '_▪️ ID:_ ' + (row.rowId || 'Não informado') + '\n'; - }); - } else { - formattedList += '\nNenhuma linha encontrada nesta seção.\n'; - } - }); - } else { - formattedList += '\nNenhuma seção encontrada.\n'; - } - - return formattedList; - } - - if (typeKey === 'listResponseMessage') { - const responseTitle = result?.title || 'Não informado'; - const responseDescription = result?.description || 'Não informado'; - const responseRowId = result?.singleSelectReply?.selectedRowId || 'Não informado'; - - const formattedResponseList = - '*Resposta da Lista:*\n\n' + - '_Título_: ' + - responseTitle + - '\n' + - '_Descrição_: ' + - responseDescription + - '\n' + - '_ID_: ' + - responseRowId; - return formattedResponseList; - } - - this.logger.verbose('message content: ' + result); - - return result; - } - - private getConversationMessage(msg: any) { - this.logger.verbose('get conversation message'); - - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - this.logger.verbose('conversation message: ' + messageContent); - - return messageContent; - } - - public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { - this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); - try { - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (!waInstance) { - this.logger.warn('wa instance not found'); - return null; - } - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (event === 'messages.upsert' || event === 'send.message') { - this.logger.verbose('event messages.upsert'); - - if (body.key.remoteJid === 'status@broadcast') { - this.logger.verbose('status broadcast found'); - return; - } - - this.logger.verbose('get conversation message'); - - // Whatsapp to Chatwoot - const originalMessage = await this.getConversationMessage(body.message); - const bodyMessage = originalMessage - ? originalMessage - .replaceAll(/\*((?!\s)([^\n*]+?)(? { - await img.cover(320, 180).writeAsync(fileName); - }) - .catch((err) => { - this.logger.error(`image is not write: ${err}`); - }); - const truncStr = (str: string, len: number) => { - return str.length > len ? str.substring(0, len) + '...' : str; - }; - - const title = truncStr(adsMessage.title, 40); - const description = truncStr(adsMessage.body, 75); - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData( - getConversation, - fileName, - messageType, - `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, - instance, - body, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - if (event === Events.MESSAGES_DELETE) { - this.logger.verbose('deleting message from instance: ' + instance.instanceName); - - if (!body?.key?.id) { - this.logger.warn('message id not found'); - return; - } - - const message = await this.getMessageByKeyId(instance, body.key.id); - if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { - this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); - return await client.messages.delete({ - accountId: this.provider.account_id, - conversationId: message.chatwoot.conversationId, - messageId: message.chatwoot.messageId, - }); - } - } - - if (event === 'status.instance') { - this.logger.verbose('event status.instance'); - const data = body; - const inbox = await this.getInbox(instance); - - if (!inbox) { - this.logger.warn('inbox not found'); - return; - } - - const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgStatus, 'incoming'); - } - - if (event === 'connection.update') { - this.logger.verbose('event connection.update'); - - if (body.status === 'open') { - // if we have qrcode count then we understand that a new connection was established - if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { - const msgConnection = `🚀 Connection successfully established!`; - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - } - } - } - - if (event === 'qrcode.updated') { - this.logger.verbose('event qrcode.updated'); - if (body.statusCode === 500) { - this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.`; - - this.logger.verbose('send message to chatwoot'); - return await this.createBotMessage(instance, erroQRcode, 'incoming'); - } else { - this.logger.verbose('qrcode success'); - const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; - - this.logger.verbose('temp file name: ' + fileName); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); - - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; - - if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( - 4, - 8, - )}`; - } - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgQrCode, 'incoming'); - } - } - } catch (error) { - this.logger.error(error); - } - } -} From b761fba8cbcdf0c7c6f75af7f7bc437fd185f78e Mon Sep 17 00:00:00 2001 From: jaison-x Date: Mon, 1 Jan 2024 18:11:25 -0300 Subject: [PATCH 22/36] fix(chatwoot): fix looping when deleting a message in chatwoot --- src/whatsapp/repository/message.repository.ts | 46 +++++++++++++++---- src/whatsapp/services/chatwoot.service.ts | 21 +++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index d7675977..594c757b 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -1,4 +1,4 @@ -import { opendirSync, readFileSync } from 'fs'; +import { opendirSync, readFileSync, rmSync } from 'fs'; import { join } from 'path'; import { ConfigService, StoreConf } from '../../config/env.config'; @@ -18,6 +18,19 @@ export class MessageRepository extends Repository { private readonly logger = new Logger('MessageRepository'); + public buildQuery(query: MessageQuery): MessageQuery { + for (const [o, p] of Object.entries(query?.where)) { + if (typeof p === 'object' && p !== null && !Array.isArray(p)) { + for (const [k, v] of Object.entries(p)) { + query.where[`${o}.${k}`] = v; + } + delete query.where[o]; + } + } + + return query; + } + public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise { this.logger.verbose('inserting messages'); @@ -91,14 +104,7 @@ export class MessageRepository extends Repository { this.logger.verbose('finding messages'); if (this.dbSettings.ENABLED) { this.logger.verbose('finding messages in db'); - for (const [o, p] of Object.entries(query?.where)) { - if (typeof p === 'object' && p !== null && !Array.isArray(p)) { - for (const [k, v] of Object.entries(p)) { - query.where[`${o}.${k}`] = v; - } - delete query.where[o]; - } - } + query = this.buildQuery(query); return await this.messageModel .find({ ...query.where }) @@ -197,4 +203,26 @@ export class MessageRepository extends Repository { this.logger.error(error); } } + + public async delete(query: MessageQuery) { + try { + this.logger.verbose('deleting message'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('deleting message in db'); + query = this.buildQuery(query); + + return await this.messageModel.deleteOne({ ...query.where }); + } + + this.logger.verbose('deleting message in store'); + rmSync(join(this.storePath, 'messages', query.where.owner, query.where.key.id + '.json'), { + force: true, + recursive: true, + }); + + return { deleted: { messageId: query.where.key.id } }; + } catch (error) { + return { error: error?.toString() }; + } + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 63c04b6f..71fe6844 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1021,7 +1021,18 @@ export class ChatwootService { limit: 1, }); if (message.length && message[0].key?.id) { + this.logger.verbose('deleting message in whatsapp. Message id: ' + message[0].key.id); await waInstance?.client.sendMessage(message[0].key.remoteJid, { delete: message[0].key }); + + this.logger.verbose('deleting message in repository. Message id: ' + message[0].key.id); + this.repository.message.delete({ + where: { + owner: instance.instanceName, + chatwoot: { + messageId: body.id, + }, + }, + }); } return { message: 'bot' }; } @@ -1742,6 +1753,16 @@ export class ChatwootService { const message = await this.getMessageByKeyId(instance, body.key.id); if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { + this.logger.verbose('deleting message in repository. Message id: ' + body.key.id); + this.repository.message.delete({ + where: { + key: { + id: body.key.id, + }, + owner: instance.instanceName, + }, + }); + this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); return await client.messages.delete({ accountId: this.provider.account_id, From 6c8c8ddcfc455fddd89793457f3b65186c295158 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 2 Jan 2024 12:03:11 -0300 Subject: [PATCH 23/36] version: baileys 6.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f68aa28..f0c56235 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@sentry/node": "^7.59.2", - "@whiskeysockets/baileys": "github:PurpShell/Baileys#combined", + "@whiskeysockets/baileys": "^6.5.0", "amqplib": "^0.10.3", "aws-sdk": "^2.1499.0", "axios": "^1.3.5", From 306c6dd21d4acaca7d9f385ab7c14d543622bec2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 2 Jan 2024 13:49:31 -0300 Subject: [PATCH 24/36] fix: Adjusts the quoted message, now has contextInfo in the message Raw --- CHANGELOG.md | 1 + src/whatsapp/models/message.model.ts | 1 + src/whatsapp/services/whatsapp.service.ts | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eff9da7d..0a9429a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * When possible use the original file extension * When receiving a file from whatsapp, use the original filename in chatwoot if possible * Remove message ids cache in chatwoot to use chatwoot's api itself +* Adjusts the quoted message, now has contextInfo in the message Raw # 1.6.1 (2023-12-22 11:43) diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 2b59f3a5..b0f39b76 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -29,6 +29,7 @@ export class MessageRaw { source_id?: string; source_reply_id?: string; chatwoot?: ChatwootMessage; + contextInfo?: any; } const messageSchema = new Schema({ diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 2f709e19..8f7d27d5 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1029,6 +1029,8 @@ export class WAStartupService { const localUrl = this.localWebhook.url; + console.log(data); + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendDataWebhook-global', @@ -1756,6 +1758,8 @@ export class WAStartupService { received?.message?.documentMessage || received?.message?.audioMessage; + const contentMsg = received.message[getContentType(received.message)] as any; + if (this.localWebhook.webhook_base64 === true && isMedia) { const buffer = await downloadMediaMessage( { key: received.key, message: received?.message }, @@ -1773,6 +1777,7 @@ export class WAStartupService { ...received.message, base64: buffer ? buffer.toString('base64') : undefined, }, + contextInfo: contentMsg?.contextInfo, messageType: getContentType(received.message), messageTimestamp: received.messageTimestamp as number, owner: this.instance.name, @@ -1783,6 +1788,7 @@ export class WAStartupService { key: received.key, pushName: received.pushName, message: { ...received.message }, + contextInfo: contentMsg?.contextInfo, messageType: getContentType(received.message), messageTimestamp: received.messageTimestamp as number, owner: this.instance.name, From a446df4620137f4975440bea12dae09656995ed6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 2 Jan 2024 14:04:54 -0300 Subject: [PATCH 25/36] fix: Adjusts the quoted message, now has contextInfo in the message Raw --- src/whatsapp/services/whatsapp.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 8f7d27d5..63dde743 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2489,10 +2489,13 @@ export class WAStartupService { ); })(); + const contentMsg = messageSent.message[getContentType(messageSent.message)] as any; + const messageRaw: MessageRaw = { key: messageSent.key, pushName: messageSent.pushName, message: { ...messageSent.message }, + contextInfo: contentMsg?.contextInfo, messageType: getContentType(messageSent.message), messageTimestamp: messageSent.messageTimestamp as number, owner: this.instance.name, From 7373eea842e5d2d7aa6ebb427e80607dde96515f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 3 Jan 2024 10:18:20 -0300 Subject: [PATCH 26/36] feat: Added update message endpoint --- CHANGELOG.md | 4 ++++ src/validate/validate.schema.ts | 20 ++++++++++++++++++++ src/whatsapp/controllers/chat.controller.ts | 6 ++++++ src/whatsapp/dto/chat.dto.ts | 6 ++++++ src/whatsapp/routers/chat.router.ts | 19 +++++++++++++++++++ src/whatsapp/services/whatsapp.service.ts | 16 ++++++++++++++++ 6 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a9429a5..9033f10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 1.6.2 (develop) +### Feature + +* Added update message endpoint + ### Fixed * Proxy configuration improvements diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 1ccdf125..efc6f685 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -592,6 +592,26 @@ export const profileStatusSchema: JSONSchema7 = { ...isNotEmpty('status'), }; +export const updateMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + text: { type: 'string' }, + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, + }, + ...isNotEmpty('number', 'text', 'key'), +}; + export const profilePictureSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 60a9c618..f9d77fce 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -10,6 +10,7 @@ import { ProfileStatusDto, ReadMessageDto, SendPresenceDto, + UpdateMessageDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; @@ -117,4 +118,9 @@ export class ChatController { logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); } + + public async updateMessage({ instanceName }: InstanceDto, data: UpdateMessageDto) { + logger.verbose('requested updateMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateMessage(data); + } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 07553c90..efa74db8 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -100,3 +100,9 @@ export class SendPresenceDto extends Metadata { delay: number; }; } + +export class UpdateMessageDto extends Metadata { + number: string; + key: proto.IMessageKey; + text: string; +} diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 29d1cdc3..77285d1b 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -14,6 +14,7 @@ import { profileSchema, profileStatusSchema, readMessageSchema, + updateMessageSchema, whatsappNumberSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; @@ -28,6 +29,7 @@ import { ProfileStatusDto, ReadMessageDto, SendPresenceDto, + UpdateMessageDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; @@ -364,6 +366,23 @@ export class ChatRouter extends RouterBroker { execute: (instance) => chatController.removeProfilePicture(instance), }); + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('updateMessage'), ...guards, async (req, res) => { + logger.verbose('request received in updateMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: updateMessageSchema, + ClassRef: UpdateMessageDto, + execute: (instance, data) => chatController.updateMessage(instance, data), + }); + return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 63dde743..227167f7 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -85,6 +85,7 @@ import { PrivacySettingDto, ReadMessageDto, SendPresenceDto, + UpdateMessageDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { @@ -3529,6 +3530,21 @@ export class WAStartupService { } } + public async updateMessage(data: UpdateMessageDto) { + try { + const jid = this.createJid(data.number); + + this.logger.verbose('Updating message'); + return await this.client.sendMessage(jid, { + text: data.text, + edit: data.key, + }); + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + // Group public async createGroup(create: CreateGroupDto) { this.logger.verbose('Creating group: ' + create.subject); From f3760476324c25ddab0e4b7924b2ed4118c3e629 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Wed, 3 Jan 2024 18:42:54 -0300 Subject: [PATCH 27/36] perf(chatwoot): create cache for the most used/expensive functions in chatwoot --- .../controllers/instance.controller.ts | 2 + src/whatsapp/services/cache.service.ts | 39 ++++++++++++++ src/whatsapp/services/chatwoot.service.ts | 52 ++++++++++++++++++- src/whatsapp/services/monitor.service.ts | 1 + src/whatsapp/services/whatsapp.service.ts | 10 ++++ 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/whatsapp/services/cache.service.ts diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0860f972..3c125efc 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -548,6 +548,7 @@ export class InstanceController { switch (state) { case 'open': this.logger.verbose('logging out instance: ' + instanceName); + instance.clearCacheChatwoot(); await instance.reloadConnection(); await delay(2000); @@ -613,6 +614,7 @@ export class InstanceController { } try { this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues(); + this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot(); if (instance.state === 'connecting') { this.logger.verbose('logging out instance: ' + instanceName); diff --git a/src/whatsapp/services/cache.service.ts b/src/whatsapp/services/cache.service.ts new file mode 100644 index 00000000..8a77b79b --- /dev/null +++ b/src/whatsapp/services/cache.service.ts @@ -0,0 +1,39 @@ +import NodeCache from 'node-cache'; + +import { Logger } from '../../config/logger.config'; + +export class CacheService { + private readonly logger = new Logger(CacheService.name); + + constructor(private module: string) {} + + static localCache = new NodeCache({ + stdTTL: 12 * 60 * 60, + }); + + public get(key: string) { + return CacheService.localCache.get(`${this.module}-${key}`); + } + + public set(key: string, value) { + return CacheService.localCache.set(`${this.module}-${key}`, value); + } + + public has(key: string) { + return CacheService.localCache.has(`${this.module}-${key}`); + } + + public delete(key: string) { + return CacheService.localCache.del(`${this.module}-${key}`); + } + + public deleteAll() { + const keys = CacheService.localCache.keys().filter((key) => key.substring(0, this.module.length) === this.module); + + return CacheService.localCache.del(keys); + } + + public keys() { + return CacheService.localCache.keys(); + } +} diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index a2f1c3ee..b636452a 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,4 +1,4 @@ -import ChatwootClient from '@figuro/chatwoot-sdk'; +import ChatwootClient, { conversation, inbox } from '@figuro/chatwoot-sdk'; import axios from 'axios'; import FormData from 'form-data'; import { createReadStream, unlinkSync, writeFileSync } from 'fs'; @@ -11,15 +11,17 @@ import { Logger } from '../../config/logger.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; -import { MessageRaw } from '../models'; +import { ChatwootRaw, MessageRaw } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events } from '../types/wa.types'; +import { CacheService } from './cache.service'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { private readonly logger = new Logger(ChatwootService.name); private provider: any; + private cache = new CacheService(ChatwootService.name); constructor( private readonly waMonitor: WAMonitoringService, @@ -28,6 +30,11 @@ export class ChatwootService { ) {} private async getProvider(instance: InstanceDto) { + const cacheKey = `getProvider-${instance.instanceName}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as ChatwootRaw; + } + this.logger.verbose('get provider to instance: ' + instance.instanceName); const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); @@ -38,6 +45,8 @@ export class ChatwootService { this.logger.verbose('provider found'); + this.cache.set(cacheKey, provider); + return provider; // try { // } catch (error) { @@ -60,6 +69,11 @@ export class ChatwootService { this.provider = provider; + const cacheKey = `clientCw-${instance.instanceName}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as ChatwootClient; + } + this.logger.verbose('create client to instance: ' + instance.instanceName); const client = new ChatwootClient({ config: { @@ -72,9 +86,15 @@ export class ChatwootService { this.logger.verbose('client created'); + this.cache.set(cacheKey, client); + return client; } + public getCache() { + return this.cache; + } + public async create(instance: InstanceDto, data: ChatwootDto) { this.logger.verbose('create chatwoot: ' + instance.instanceName); @@ -389,6 +409,26 @@ export class ChatwootService { return null; } + const cacheKey = `createConversation-${instance.instanceName}-${body.key.remoteJid}`; + if (this.cache.has(cacheKey)) { + const conversationId = this.cache.get(cacheKey) as number; + let conversationExists: conversation | boolean; + try { + conversationExists = await client.conversations.get({ + accountId: this.provider.account_id, + conversationId: conversationId, + }); + } catch (error) { + conversationExists = false; + } + if (!conversationExists) { + this.cache.delete(cacheKey); + return await this.createConversation(instance, body); + } + + return conversationId; + } + const isGroup = body.key.remoteJid.includes('@g.us'); this.logger.verbose('is group: ' + isGroup); @@ -539,6 +579,7 @@ export class ChatwootService { if (conversation) { this.logger.verbose('conversation found'); + this.cache.set(cacheKey, conversation.id); return conversation.id; } } @@ -564,6 +605,7 @@ export class ChatwootService { } this.logger.verbose('conversation created'); + this.cache.set(cacheKey, conversation.id); return conversation.id; } catch (error) { this.logger.error(error); @@ -573,6 +615,11 @@ export class ChatwootService { public async getInbox(instance: InstanceDto) { this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const cacheKey = `getInbox-${instance.instanceName}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as inbox; + } + const client = await this.clientCw(instance); if (!client) { @@ -599,6 +646,7 @@ export class ChatwootService { } this.logger.verbose('return inbox'); + this.cache.set(cacheKey, findByName); return findByName; } diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 0191079c..ecf70586 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -442,6 +442,7 @@ export class WAMonitoringService { this.eventEmitter.on('logout.instance', async (instanceName: string) => { this.logger.verbose('logout instance: ' + instanceName); try { + this.waInstances[instanceName]?.clearCacheChatwoot(); this.logger.verbose('request cleaning up instance: ' + instanceName); this.cleaningUp(instanceName); } finally { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 227167f7..1d8d796e 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -368,6 +368,8 @@ export class WAStartupService { Object.assign(this.localChatwoot, { ...data, sign_delimiter: data.sign_msg ? data.sign_delimiter : null }); + this.clearCacheChatwoot(); + this.logger.verbose('Chatwoot set'); } @@ -402,6 +404,14 @@ export class WAStartupService { }; } + public clearCacheChatwoot() { + this.logger.verbose('Removing cache from chatwoot'); + + if (this.localChatwoot.enabled) { + this.chatwootService.getCache().deleteAll(); + } + } + private async loadSettings() { this.logger.verbose('Loading settings'); const data = await this.repository.settings.find(this.instanceName); From 3ddff84be042a8a8cdc85bea17b705a84a5a4020 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 7 Jan 2024 19:10:56 -0300 Subject: [PATCH 28/36] test: docker with platform arm --- .github/workflows/publish_docker_image.yml | 64 ++++++++++++++++++++++ src/whatsapp/services/whatsapp.service.ts | 2 - 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish_docker_image.yml diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml new file mode 100644 index 00000000..8419d7dc --- /dev/null +++ b/.github/workflows/publish_docker_image.yml @@ -0,0 +1,64 @@ +name: Build Docker image + +on: + push: + tags: ['v*'] + +jobs: + build-amd: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Extract existing image metadata + id: image-meta + uses: docker/metadata-action@v4 + with: + images: atendai/evolution-api + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push AMD image + uses: docker/build-push-action@v4 + with: + context: . + labels: ${{ steps.image-meta.outputs.labels }} + platforms: linux/amd64 + push: true + + build-arm: + runs-on: buildjet-4vcpu-ubuntu-2204-arm + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Extract existing image metadata + id: image-meta + uses: docker/metadata-action@v4 + with: + images: atendai/evolution-api + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push ARM image + uses: docker/build-push-action@v4 + with: + context: . + labels: ${{ steps.image-meta.outputs.labels }} + platforms: linux/arm64 + push: true diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 227167f7..4c76c92a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1030,8 +1030,6 @@ export class WAStartupService { const localUrl = this.localWebhook.url; - console.log(data); - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { local: WAStartupService.name + '.sendDataWebhook-global', From 0bc12733a37fa38ab6493b6531b6667729bfdf4f Mon Sep 17 00:00:00 2001 From: jaison-x Date: Mon, 8 Jan 2024 22:20:16 -0300 Subject: [PATCH 29/36] fix(chatwoot): invalidate the conversation cache if reopen_conversation is false and the conversation was resolved --- src/whatsapp/services/chatwoot.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index b636452a..bf22892b 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1037,6 +1037,17 @@ export class ChatwootService { return null; } + // invalidate the conversation cache if reopen_conversation is false and the conversation was resolved + if ( + this.provider.reopen_conversation === false && + body.event === 'conversation_status_changed' && + body.status === 'resolved' && + body.meta?.sender?.identifier + ) { + const keyToDelete = `createConversation-${instance.instanceName}-${body.meta.sender.identifier}`; + this.cache.delete(keyToDelete); + } + this.logger.verbose('check if is bot'); if ( !body?.conversation || From 933d78710872572e7239cbd3fef9abe27f287b2a Mon Sep 17 00:00:00 2001 From: Alan Martines Date: Wed, 10 Jan 2024 09:27:25 -0400 Subject: [PATCH 30/36] Fix: Corrects the generation of the hash variable name, error when generating with white spaces and special characters. --- src/whatsapp/services/whatsapp.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f222d5b7..cd107044 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2844,7 +2844,8 @@ export class WAStartupService { this.logger.verbose('Processing audio'); let tempAudioPath: string; let outputAudio: string; - + + number = number.replace(/\D/g, ""); const hash = `${number}-${new Date().getTime()}`; this.logger.verbose('Hash to audio name: ' + hash); From 2d8b5f04e95357073cb03bc7b66671eaa19e4208 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Fri, 12 Jan 2024 15:58:11 -0300 Subject: [PATCH 31/36] feat(cacheservice): add suport to use use redis in cacheservice --- src/config/env.config.ts | 24 ++++++ src/dev-env.yml | 11 +++ src/libs/cacheengine.ts | 22 +++++ src/libs/localcache.ts | 48 +++++++++++ src/libs/rediscache.client.ts | 59 +++++++++++++ src/libs/rediscache.ts | 83 +++++++++++++++++++ src/whatsapp/abstract/abstract.cache.ts | 13 +++ .../controllers/chatwoot.controller.ts | 6 +- .../controllers/instance.controller.ts | 10 ++- src/whatsapp/services/cache.service.ts | 67 ++++++++++----- src/whatsapp/services/chatwoot.service.ts | 31 +++---- src/whatsapp/services/monitor.service.ts | 10 ++- src/whatsapp/services/whatsapp.service.ts | 6 +- src/whatsapp/whatsapp.module.ts | 9 +- 14 files changed, 351 insertions(+), 48 deletions(-) create mode 100644 src/libs/cacheengine.ts create mode 100644 src/libs/localcache.ts create mode 100644 src/libs/rediscache.client.ts create mode 100644 src/libs/rediscache.ts create mode 100644 src/whatsapp/abstract/abstract.cache.ts diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 511f4ebc..71a1a497 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -132,11 +132,22 @@ export type GlobalWebhook = { ENABLED: boolean; WEBHOOK_BY_EVENTS: boolean; }; +export type CacheConfRedis = { + ENABLED: boolean; + URI: string; + PREFIX_KEY: string; + TTL: number; +}; +export type CacheConfLocal = { + ENABLED: boolean; + TTL: number; +}; export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; export type ConfigSessionPhone = { CLIENT: string; NAME: string }; export type QrCode = { LIMIT: number; COLOR: string }; export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean }; +export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type Production = boolean; export interface Env { @@ -156,6 +167,7 @@ export interface Env { CONFIG_SESSION_PHONE: ConfigSessionPhone; QRCODE: QrCode; TYPEBOT: Typebot; + CACHE: CacheConf; AUTHENTICATION: Auth; PRODUCTION?: Production; } @@ -318,6 +330,18 @@ export class ConfigService { API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true', }, + CACHE: { + REDIS: { + ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', + URI: process.env.CACHE_REDIS_URI || '', + PREFIX_KEY: process.env.CACHE_REDIS_PREFIX_KEY || 'evolution-cache', + TTL: Number.parseInt(process.env.CACHE_REDIS_TTL) || 604800, + }, + LOCAL: { + ENABLED: process.env?.CACHE_LOCAL_ENABLED === 'true', + TTL: Number.parseInt(process.env.CACHE_REDIS_TTL) || 86400, + }, + }, AUTHENTICATION: { TYPE: process.env.AUTHENTICATION_TYPE as 'apikey', API_KEY: { diff --git a/src/dev-env.yml b/src/dev-env.yml index 80c7e376..42438aff 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -153,6 +153,17 @@ TYPEBOT: API_VERSION: 'old' # old | latest KEEP_OPEN: false +# Cache to optimize application performance +CACHE: + REDIS: + ENABLED: false + URI: "redis://localhost:6379" + PREFIX_KEY: "evolution-cache" + TTL: 604800 + LOCAL: + ENABLED: false + TTL: 86400 + # Defines an authentication type for the api # We recommend using the apikey because it will allow you to use a custom token, # if you use jwt, a random token will be generated and may be expired and you will have to generate a new token diff --git a/src/libs/cacheengine.ts b/src/libs/cacheengine.ts new file mode 100644 index 00000000..a22d7e68 --- /dev/null +++ b/src/libs/cacheengine.ts @@ -0,0 +1,22 @@ +import { CacheConf, ConfigService } from '../config/env.config'; +import { ICache } from '../whatsapp/abstract/abstract.cache'; +import { LocalCache } from './localcache'; +import { RedisCache } from './rediscache'; + +export class CacheEngine { + private engine: ICache; + + constructor(private readonly configService: ConfigService, module: string) { + const cacheConf = configService.get('CACHE'); + + if (cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') { + this.engine = new RedisCache(configService, module); + } else if (cacheConf?.LOCAL?.ENABLED) { + this.engine = new LocalCache(configService, module); + } + } + + public getEngine() { + return this.engine; + } +} diff --git a/src/libs/localcache.ts b/src/libs/localcache.ts new file mode 100644 index 00000000..fe1f295f --- /dev/null +++ b/src/libs/localcache.ts @@ -0,0 +1,48 @@ +import NodeCache from 'node-cache'; + +import { CacheConf, CacheConfLocal, ConfigService } from '../config/env.config'; +import { ICache } from '../whatsapp/abstract/abstract.cache'; + +export class LocalCache implements ICache { + private conf: CacheConfLocal; + static localCache = new NodeCache(); + + constructor(private readonly configService: ConfigService, private readonly module: string) { + this.conf = this.configService.get('CACHE')?.LOCAL; + } + + async get(key: string): Promise { + return LocalCache.localCache.get(this.buildKey(key)); + } + + async set(key: string, value: any, ttl?: number) { + return LocalCache.localCache.set(this.buildKey(key), value, ttl || this.conf.TTL); + } + + async has(key: string) { + return LocalCache.localCache.has(this.buildKey(key)); + } + + async delete(key: string) { + return LocalCache.localCache.del(this.buildKey(key)); + } + + async deleteAll(appendCriteria?: string) { + const keys = await this.keys(appendCriteria); + if (!keys?.length) { + return 0; + } + + return LocalCache.localCache.del(keys); + } + + async keys(appendCriteria?: string) { + const filter = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}`; + + return LocalCache.localCache.keys().filter((key) => key.substring(0, filter.length) === filter); + } + + buildKey(key: string) { + return `${this.module}:${key}`; + } +} diff --git a/src/libs/rediscache.client.ts b/src/libs/rediscache.client.ts new file mode 100644 index 00000000..b3f8dead --- /dev/null +++ b/src/libs/rediscache.client.ts @@ -0,0 +1,59 @@ +import { createClient, RedisClientType } from 'redis'; + +import { CacheConf, CacheConfRedis, configService } from '../config/env.config'; +import { Logger } from '../config/logger.config'; + +class Redis { + private logger = new Logger(Redis.name); + private client: RedisClientType = null; + private conf: CacheConfRedis; + private connected = false; + + constructor() { + this.conf = configService.get('CACHE')?.REDIS; + } + + getConnection(): RedisClientType { + if (this.connected) { + return this.client; + } else { + this.client = createClient({ + url: this.conf.URI, + }); + + this.client.on('connect', () => { + this.logger.verbose('redis connecting'); + }); + + this.client.on('ready', () => { + this.logger.verbose('redis ready'); + this.connected = true; + }); + + this.client.on('error', () => { + this.logger.error('redis disconnected'); + this.connected = false; + }); + + this.client.on('end', () => { + this.logger.verbose('redis connection ended'); + this.connected = false; + }); + + try { + this.logger.verbose('connecting new redis client'); + this.client.connect(); + this.connected = true; + this.logger.verbose('connected to new redis client'); + } catch (e) { + this.connected = false; + this.logger.error('redis connect exception caught: ' + e); + return null; + } + + return this.client; + } + } +} + +export const redisClient = new Redis(); diff --git a/src/libs/rediscache.ts b/src/libs/rediscache.ts new file mode 100644 index 00000000..cd0b1283 --- /dev/null +++ b/src/libs/rediscache.ts @@ -0,0 +1,83 @@ +import { RedisClientType } from 'redis'; + +import { CacheConf, CacheConfRedis, ConfigService } from '../config/env.config'; +import { Logger } from '../config/logger.config'; +import { ICache } from '../whatsapp/abstract/abstract.cache'; +import { redisClient } from './rediscache.client'; + +export class RedisCache implements ICache { + private readonly logger = new Logger(RedisCache.name); + private client: RedisClientType; + private conf: CacheConfRedis; + + constructor(private readonly configService: ConfigService, private readonly module: string) { + this.conf = this.configService.get('CACHE')?.REDIS; + this.client = redisClient.getConnection(); + } + + async get(key: string): Promise { + try { + return JSON.parse(await this.client.get(this.buildKey(key))); + } catch (error) { + this.logger.error(error); + } + } + + async set(key: string, value: any, ttl?: number) { + try { + await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value)); + } catch (error) { + this.logger.error(error); + } + } + + async has(key: string) { + try { + return (await this.client.exists(this.buildKey(key))) > 0; + } catch (error) { + this.logger.error(error); + } + } + + async delete(key: string) { + try { + return await this.client.del(this.buildKey(key)); + } catch (error) { + this.logger.error(error); + } + } + + async deleteAll(appendCriteria?: string) { + try { + const keys = await this.keys(appendCriteria); + if (!keys?.length) { + return 0; + } + + return await this.client.del(keys); + } catch (error) { + this.logger.error(error); + } + } + + async keys(appendCriteria?: string) { + try { + const match = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}*`; + const keys = []; + for await (const key of this.client.scanIterator({ + MATCH: match, + COUNT: 100, + })) { + keys.push(key); + } + + return [...new Set(keys)]; + } catch (error) { + this.logger.error(error); + } + } + + buildKey(key: string) { + return `${this.conf?.PREFIX_KEY}:${this.module}:${key}`; + } +} diff --git a/src/whatsapp/abstract/abstract.cache.ts b/src/whatsapp/abstract/abstract.cache.ts new file mode 100644 index 00000000..caad2691 --- /dev/null +++ b/src/whatsapp/abstract/abstract.cache.ts @@ -0,0 +1,13 @@ +export interface ICache { + get(key: string): Promise; + + set(key: string, value: any, ttl?: number): void; + + has(key: string): Promise; + + keys(appendCriteria?: string): Promise; + + delete(key: string | string[]): Promise; + + deleteAll(appendCriteria?: string): Promise; +} diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 8f59ccac..2de472ff 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -3,9 +3,11 @@ import { isURL } from 'class-validator'; import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; +import { CacheEngine } from '../../libs/cacheengine'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; +import { CacheService } from '../services/cache.service'; import { ChatwootService } from '../services/chatwoot.service'; import { waMonitor } from '../whatsapp.module'; @@ -94,7 +96,9 @@ export class ChatwootController { public async receiveWebhook(instance: InstanceDto, data: any) { logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); + + const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine()); + const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository, chatwootCache); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 3c125efc..8bba80aa 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -10,6 +10,7 @@ import { RedisCache } from '../../libs/redis.client'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.service'; +import { CacheService } from '../services/cache.service'; import { ChatwootService } from '../services/chatwoot.service'; import { WAMonitoringService } from '../services/monitor.service'; import { RabbitmqService } from '../services/rabbitmq.service'; @@ -36,6 +37,7 @@ export class InstanceController { private readonly sqsService: SqsService, private readonly typebotService: TypebotService, private readonly cache: RedisCache, + private readonly chatwootCache: CacheService, ) {} private readonly logger = new Logger(InstanceController.name); @@ -82,7 +84,13 @@ export class InstanceController { await this.authService.checkDuplicateToken(token); this.logger.verbose('creating instance'); - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + const instance = new WAStartupService( + this.configService, + this.eventEmitter, + this.repository, + this.cache, + this.chatwootCache, + ); instance.instanceName = instanceName; const instanceId = v4(); diff --git a/src/whatsapp/services/cache.service.ts b/src/whatsapp/services/cache.service.ts index 8a77b79b..0db39a44 100644 --- a/src/whatsapp/services/cache.service.ts +++ b/src/whatsapp/services/cache.service.ts @@ -1,39 +1,62 @@ -import NodeCache from 'node-cache'; - import { Logger } from '../../config/logger.config'; +import { ICache } from '../abstract/abstract.cache'; export class CacheService { private readonly logger = new Logger(CacheService.name); - constructor(private module: string) {} - - static localCache = new NodeCache({ - stdTTL: 12 * 60 * 60, - }); - - public get(key: string) { - return CacheService.localCache.get(`${this.module}-${key}`); + constructor(private readonly cache: ICache) { + if (cache) { + this.logger.verbose(`cacheservice created using cache engine: ${cache.constructor?.name}`); + } else { + this.logger.verbose(`cacheservice disabled`); + } } - public set(key: string, value) { - return CacheService.localCache.set(`${this.module}-${key}`, value); + async get(key: string): Promise { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice getting key: ${key}`); + return this.cache.get(key); } - public has(key: string) { - return CacheService.localCache.has(`${this.module}-${key}`); + async set(key: string, value: any) { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice setting key: ${key}`); + this.cache.set(key, value); } - public delete(key: string) { - return CacheService.localCache.del(`${this.module}-${key}`); + async has(key: string) { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice has key: ${key}`); + return this.cache.has(key); } - public deleteAll() { - const keys = CacheService.localCache.keys().filter((key) => key.substring(0, this.module.length) === this.module); - - return CacheService.localCache.del(keys); + async delete(key: string) { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice deleting key: ${key}`); + return this.cache.delete(key); } - public keys() { - return CacheService.localCache.keys(); + async deleteAll(appendCriteria?: string) { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice deleting all keys`); + return this.cache.deleteAll(appendCriteria); + } + + async keys(appendCriteria?: string) { + if (!this.cache) { + return; + } + this.logger.verbose(`cacheservice getting all keys`); + return this.cache.keys(appendCriteria); } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index bf22892b..ad1e9ef5 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -8,31 +8,31 @@ import path from 'path'; import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { ICache } from '../abstract/abstract.cache'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; import { ChatwootRaw, MessageRaw } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events } from '../types/wa.types'; -import { CacheService } from './cache.service'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { private readonly logger = new Logger(ChatwootService.name); private provider: any; - private cache = new CacheService(ChatwootService.name); constructor( private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService, private readonly repository: RepositoryBroker, + private readonly cache: ICache, ) {} private async getProvider(instance: InstanceDto) { - const cacheKey = `getProvider-${instance.instanceName}`; - if (this.cache.has(cacheKey)) { - return this.cache.get(cacheKey) as ChatwootRaw; + const cacheKey = `${instance.instanceName}:getProvider`; + if (await this.cache.has(cacheKey)) { + return (await this.cache.get(cacheKey)) as ChatwootRaw; } this.logger.verbose('get provider to instance: ' + instance.instanceName); @@ -69,11 +69,6 @@ export class ChatwootService { this.provider = provider; - const cacheKey = `clientCw-${instance.instanceName}`; - if (this.cache.has(cacheKey)) { - return this.cache.get(cacheKey) as ChatwootClient; - } - this.logger.verbose('create client to instance: ' + instance.instanceName); const client = new ChatwootClient({ config: { @@ -86,8 +81,6 @@ export class ChatwootService { this.logger.verbose('client created'); - this.cache.set(cacheKey, client); - return client; } @@ -409,9 +402,9 @@ export class ChatwootService { return null; } - const cacheKey = `createConversation-${instance.instanceName}-${body.key.remoteJid}`; - if (this.cache.has(cacheKey)) { - const conversationId = this.cache.get(cacheKey) as number; + const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`; + if (await this.cache.has(cacheKey)) { + const conversationId = (await this.cache.get(cacheKey)) as number; let conversationExists: conversation | boolean; try { conversationExists = await client.conversations.get({ @@ -615,9 +608,9 @@ export class ChatwootService { public async getInbox(instance: InstanceDto) { this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const cacheKey = `getInbox-${instance.instanceName}`; - if (this.cache.has(cacheKey)) { - return this.cache.get(cacheKey) as inbox; + const cacheKey = `${instance.instanceName}:getInbox`; + if (await this.cache.has(cacheKey)) { + return (await this.cache.get(cacheKey)) as inbox; } const client = await this.clientCw(instance); @@ -1044,7 +1037,7 @@ export class ChatwootService { body.status === 'resolved' && body.meta?.sender?.identifier ) { - const keyToDelete = `createConversation-${instance.instanceName}-${body.meta.sender.identifier}`; + const keyToDelete = `${instance.instanceName}:createConversation-${body.meta.sender.identifier}`; this.cache.delete(keyToDelete); } diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index ecf70586..3c3e8881 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -26,6 +26,7 @@ import { WebsocketModel, } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; +import { CacheService } from './cache.service'; import { WAStartupService } from './whatsapp.service'; export class WAMonitoringService { @@ -34,6 +35,7 @@ export class WAMonitoringService { private readonly configService: ConfigService, private readonly repository: RepositoryBroker, private readonly cache: RedisCache, + private readonly chatwootCache: CacheService, ) { this.logger.verbose('instance created'); @@ -359,7 +361,13 @@ export class WAMonitoringService { } private async setInstance(name: string) { - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + const instance = new WAStartupService( + this.configService, + this.eventEmitter, + this.repository, + this.cache, + this.chatwootCache, + ); instance.instanceName = name; this.logger.verbose('Instance loaded: ' + name); await instance.connectToWhatsapp(); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index c6aa06b6..efe4e2a3 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -131,6 +131,7 @@ import { MessageUpQuery } from '../repository/messageUp.repository'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; +import { CacheService } from './cache.service'; import { ChamaaiService } from './chamaai.service'; import { ChatwootService } from './chatwoot.service'; import { TypebotService } from './typebot.service'; @@ -143,6 +144,7 @@ export class WAStartupService { private readonly eventEmitter: EventEmitter2, private readonly repository: RepositoryBroker, private readonly cache: RedisCache, + private readonly chatwootCache: CacheService, ) { this.logger.verbose('WAStartupService initialized'); this.cleanStore(); @@ -170,7 +172,7 @@ export class WAStartupService { private phoneNumber: string; - private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); + private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository, this.chatwootCache); private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); @@ -408,7 +410,7 @@ export class WAStartupService { this.logger.verbose('Removing cache from chatwoot'); if (this.localChatwoot.enabled) { - this.chatwootService.getCache().deleteAll(); + this.chatwootService.getCache()?.deleteAll(this.instanceName); } } diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index d459bf6a..0b5da554 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -1,6 +1,7 @@ import { configService } from '../config/env.config'; import { eventEmitter } from '../config/event.config'; import { Logger } from '../config/logger.config'; +import { CacheEngine } from '../libs/cacheengine'; import { dbserver } from '../libs/db.connect'; import { RedisCache } from '../libs/redis.client'; import { ChamaaiController } from './controllers/chamaai.controller'; @@ -48,6 +49,7 @@ import { TypebotRepository } from './repository/typebot.repository'; import { WebhookRepository } from './repository/webhook.repository'; import { WebsocketRepository } from './repository/websocket.repository'; import { AuthService } from './services/auth.service'; +import { CacheService } from './services/cache.service'; import { ChamaaiService } from './services/chamaai.service'; import { ChatwootService } from './services/chatwoot.service'; import { WAMonitoringService } from './services/monitor.service'; @@ -97,7 +99,9 @@ export const repository = new RepositoryBroker( export const cache = new RedisCache(); -export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache); +const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine()); + +export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache, chatwootCache); const authService = new AuthService(configService, waMonitor, repository); @@ -129,7 +133,7 @@ const sqsService = new SqsService(waMonitor); export const sqsController = new SqsController(sqsService); -const chatwootService = new ChatwootService(waMonitor, configService, repository); +const chatwootService = new ChatwootService(waMonitor, configService, repository, chatwootCache); export const chatwootController = new ChatwootController(chatwootService, configService, repository); @@ -151,6 +155,7 @@ export const instanceController = new InstanceController( sqsService, typebotService, cache, + chatwootCache, ); export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); From 095a8aa9dd8751069a6f9f331335e267195222f7 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Fri, 12 Jan 2024 16:15:44 -0300 Subject: [PATCH 32/36] fix: process.env can be null --- src/config/env.config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 71a1a497..fde4a073 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -333,13 +333,13 @@ export class ConfigService { CACHE: { REDIS: { ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', - URI: process.env.CACHE_REDIS_URI || '', - PREFIX_KEY: process.env.CACHE_REDIS_PREFIX_KEY || 'evolution-cache', - TTL: Number.parseInt(process.env.CACHE_REDIS_TTL) || 604800, + URI: process.env?.CACHE_REDIS_URI || '', + PREFIX_KEY: process.env?.CACHE_REDIS_PREFIX_KEY || 'evolution-cache', + TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 604800, }, LOCAL: { ENABLED: process.env?.CACHE_LOCAL_ENABLED === 'true', - TTL: Number.parseInt(process.env.CACHE_REDIS_TTL) || 86400, + TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 86400, }, }, AUTHENTICATION: { From c75dfcd4993fa3bf5d7e76d792b26468e1091973 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Mon, 15 Jan 2024 20:20:02 -0300 Subject: [PATCH 33/36] feat(chatwoot): read messages from whatsapp in chatwoot It works only with messages read from whatsapp to chatwoot. --- src/whatsapp/models/message.model.ts | 2 + src/whatsapp/services/chatwoot.service.ts | 67 ++++++++++++++++++++--- src/whatsapp/services/whatsapp.service.ts | 11 +++- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index b0f39b76..9c7ac9dc 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -14,6 +14,7 @@ class ChatwootMessage { messageId?: number; inboxId?: number; conversationId?: number; + contactInbox?: { sourceId: string }; } export class MessageRaw { @@ -51,6 +52,7 @@ const messageSchema = new Schema({ messageId: { type: Number }, inboxId: { type: Number }, conversationId: { type: Number }, + contactInbox: { type: Object }, }, }); diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index ad1e9ef5..e418b904 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,4 +1,5 @@ -import ChatwootClient, { conversation, inbox } from '@figuro/chatwoot-sdk'; +import ChatwootClient, { ChatwootAPIConfig, contact, conversation, inbox } from '@figuro/chatwoot-sdk'; +import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; import axios from 'axios'; import FormData from 'form-data'; import { createReadStream, unlinkSync, writeFileSync } from 'fs'; @@ -71,12 +72,7 @@ export class ChatwootService { this.logger.verbose('create client to instance: ' + instance.instanceName); const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, + config: this.getClientCwConfig(), }); this.logger.verbose('client created'); @@ -84,6 +80,15 @@ export class ChatwootService { return client; } + public getClientCwConfig(): ChatwootAPIConfig { + return { + basePath: this.provider.url, + with_credentials: true, + credentials: 'include', + token: this.provider.token, + }; + } + public getCache() { return this.cache; } @@ -1200,6 +1205,9 @@ export class ChatwootService { messageId: body.id, inboxId: body.inbox?.id, conversationId: body.conversation?.id, + contactInbox: { + sourceId: body.conversation?.contact_inbox?.source_id, + }, }, instance, ); @@ -1231,6 +1239,9 @@ export class ChatwootService { messageId: body.id, inboxId: body.inbox?.id, conversationId: body.conversation?.id, + contactInbox: { + sourceId: body.conversation?.contact_inbox?.source_id, + }, }, instance, ); @@ -1911,6 +1922,44 @@ export class ChatwootService { } } + if (event === 'messages.read') { + this.logger.verbose('read message from instance: ' + instance.instanceName); + + if (!body?.key?.id || !body?.key?.remoteJid) { + this.logger.warn('message id not found'); + return; + } + + const message = await this.getMessageByKeyId(instance, body.key.id); + const { conversationId, contactInbox } = message?.chatwoot || {}; + if (conversationId) { + let sourceId = contactInbox?.sourceId; + const inbox = (await this.getInbox(instance)) as inbox & { + inbox_identifier?: string; + }; + + if (!sourceId && inbox) { + const contact = (await this.findContact( + instance, + this.getNumberFromRemoteJid(body.key.remoteJid), + )) as contact; + const contactInbox = contact?.contact_inboxes?.find((contactInbox) => contactInbox?.inbox?.id === inbox.id); + sourceId = contactInbox?.source_id; + } + + if (sourceId && inbox?.inbox_identifier) { + const url = + `/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` + + `/conversations/${conversationId}/update_last_seen`; + chatwootRequest(this.getClientCwConfig(), { + method: 'POST', + url: url, + }); + } + } + return; + } + if (event === 'status.instance') { this.logger.verbose('event status.instance'); const data = body; @@ -1981,4 +2030,8 @@ export class ChatwootService { this.logger.error(error); } } + + public getNumberFromRemoteJid(remoteJid: string) { + return remoteJid.replace(/:\d+/, '').split('@')[0]; + } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index efe4e2a3..3aa669e1 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1932,6 +1932,13 @@ export class WAStartupService { this.logger.verbose('group ignored'); return; } + + if (status[update.status] === 'READ' && key.fromMe) { + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp('messages.read', { instanceName: this.instance.name }, { key: key }); + } + } + // if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { if (key.remoteJid !== 'status@broadcast') { this.logger.verbose('Message update is valid'); @@ -2877,8 +2884,8 @@ export class WAStartupService { this.logger.verbose('Processing audio'); let tempAudioPath: string; let outputAudio: string; - - number = number.replace(/\D/g, ""); + + number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; this.logger.verbose('Hash to audio name: ' + hash); From f182436673929942daca166ad5ef4af4f02eda5e Mon Sep 17 00:00:00 2001 From: jaison-x Date: Wed, 17 Jan 2024 17:50:31 -0300 Subject: [PATCH 34/36] fix: message 'connection successfully' spamming --- src/whatsapp/services/chatwoot.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index e418b904..ef70cf63 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1985,6 +1985,7 @@ export class ChatwootService { const msgConnection = `🚀 Connection successfully established!`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgConnection, 'incoming'); + this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0; } } } From 5f795136171c44d1a74f2e39b92a29636fd48112 Mon Sep 17 00:00:00 2001 From: Leandro Rocha Date: Thu, 18 Jan 2024 20:25:58 -0300 Subject: [PATCH 35/36] fix: Sending status message --- src/whatsapp/services/whatsapp.service.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f222d5b7..369c0626 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2420,21 +2420,6 @@ export class WAStartupService { option as unknown as MiscMessageGenerationOptions, ); } - - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } } if (message['conversation']) { this.logger.verbose('Sending message'); @@ -3097,6 +3082,8 @@ export class WAStartupService { if (!group) throw new BadRequestException('Group not found'); onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else if (jid === 'status@broadcast') { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); } else { jid = !jid.startsWith('+') ? `+${jid}` : jid; const verify = await this.client.onWhatsApp(jid); From 4357fcf7eff9e71e6190b37c7bf0c25c9598cf67 Mon Sep 17 00:00:00 2001 From: Leandro Santos Rocha Date: Thu, 18 Jan 2024 21:30:49 -0300 Subject: [PATCH 36/36] Adjusted so that when it is "list", it sends correctly --- src/whatsapp/services/whatsapp.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 369c0626..f7530245 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2434,6 +2434,21 @@ export class WAStartupService { ); } + if (!message['audio'] && sender != 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, + }, + mentions, + }, + option as unknown as MiscMessageGenerationOptions, + ); + } + if (sender === 'status@broadcast') { this.logger.verbose('Sending message'); return await this.client.sendMessage(