From 3bb4217a45103fe5a8edb02d8fbb95f5e1db5f17 Mon Sep 17 00:00:00 2001 From: w3nder Date: Sun, 24 Dec 2023 14:32:05 -0300 Subject: [PATCH 001/144] =?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 002/144] 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 003/144] 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 004/144] 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 005/144] 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 006/144] 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 007/144] 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 008/144] 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 009/144] 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 010/144] 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 011/144] 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 012/144] 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 013/144] 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 014/144] 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 015/144] 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 016/144] 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 017/144] 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 018/144] 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 019/144] 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 020/144] 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 021/144] 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 022/144] 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 023/144] 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 024/144] 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 025/144] 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 026/144] 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 027/144] 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 028/144] 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 029/144] 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 030/144] 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 031/144] 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 032/144] 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 033/144] 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 034/144] 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 035/144] 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 036/144] 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( From 96be63f50bfd8aa5072b407d9eec9a4bd7578af8 Mon Sep 17 00:00:00 2001 From: edisoncm-ti Date: Sat, 20 Jan 2024 11:09:05 -0300 Subject: [PATCH 037/144] fix: remove quebra de linha no index final do array quando usado variavel no Typebot --- src/whatsapp/services/typebot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index b5640240..96483b98 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' || element.type === 'a') { + if (element.children && (element.type === 'p' || element.type === 'a' || element.type === 'inline-variable' || element.type === 'variable')) { for (const child of element.children) { text += applyFormatting(child); } From 440ff2f3eac85e5c8dbd2fc50f7d9ca0882eca84 Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Sat, 20 Jan 2024 11:44:10 -0300 Subject: [PATCH 038/144] Add number parameter to OnWhatsAppDto constructor --- src/whatsapp/dto/chat.dto.ts | 7 ++++++- src/whatsapp/services/whatsapp.service.ts | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index efa74db8..dc0584bb 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,7 +1,12 @@ import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { - constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} + constructor( + public readonly jid: string, + public readonly exists: boolean, + public readonly number: string, + public readonly name?: string, + ) {} } export class getBase64FromMediaMessageDto { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index c49df6bf..80ac7397 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3137,9 +3137,9 @@ export class WAStartupService { if (!group) throw new BadRequestException('Group not found'); - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject)); } else if (jid === 'status@broadcast') { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); + onWhatsapp.push(new OnWhatsAppDto(jid, false, number)); } else { jid = !jid.startsWith('+') ? `+${jid}` : jid; const verify = await this.client.onWhatsApp(jid); @@ -3147,9 +3147,9 @@ export class WAStartupService { const result = verify[0]; if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); + onWhatsapp.push(new OnWhatsAppDto(jid, false, number)); } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists, number)); } } } From c9b0e6664158b706e669cb03b30636eabbd809ea Mon Sep 17 00:00:00 2001 From: edisoncm-ti Date: Sat, 20 Jan 2024 18:28:00 -0300 Subject: [PATCH 039/144] feat: Obter a resposta de um sendList (Lista) e prosseguir o fluxo Typebot --- src/whatsapp/services/typebot.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index 96483b98..36645f63 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -274,6 +274,7 @@ export class TypebotService { const types = { conversation: msg.conversation, extendedTextMessage: msg.extendedTextMessage?.text, + responseRowId: msg.listResponseMessage.singleSelectReply?.selectedRowId, }; this.logger.verbose('type message: ' + types); From 1a633dcf105656db7ca6e25a73cb29cb37414ab0 Mon Sep 17 00:00:00 2001 From: edisoncm-ti Date: Sat, 20 Jan 2024 18:31:26 -0300 Subject: [PATCH 040/144] feat: obter a resposta de um sendList (Lista) e prosseguir o fluxo Typebot --- src/whatsapp/services/typebot.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index 96483b98..36645f63 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -274,6 +274,7 @@ export class TypebotService { const types = { conversation: msg.conversation, extendedTextMessage: msg.extendedTextMessage?.text, + responseRowId: msg.listResponseMessage.singleSelectReply?.selectedRowId, }; this.logger.verbose('type message: ' + types); From e19e37eef48aa78bb14f1369f9a1b3007771dacb Mon Sep 17 00:00:00 2001 From: Leandro Rocha Date: Sun, 21 Jan 2024 02:13:24 -0300 Subject: [PATCH 041/144] Join in Group by Invite Code --- package.json | 2 +- src/validate/validate.schema.ts | 10 ++++++++++ src/whatsapp/controllers/group.controller.ts | 6 ++++++ src/whatsapp/dto/group.dto.ts | 4 ++++ src/whatsapp/routers/group.router.ts | 18 ++++++++++++++++++ src/whatsapp/services/whatsapp.service.ts | 11 +++++++++++ 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f0c56235..828e0d1b 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": "^6.5.0", + "@whiskeysockets/baileys": "6.5.0", "amqplib": "^0.10.3", "aws-sdk": "^2.1499.0", "axios": "^1.3.5", diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index efc6f685..498d9982 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -771,6 +771,16 @@ export const groupInviteSchema: JSONSchema7 = { ...isNotEmpty('inviteCode'), }; +export const AcceptGroupInviteSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, + }, + required: ['inviteCode'], + ...isNotEmpty('inviteCode'), +}; + export const updateParticipantsSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index 0cf093ca..e452bc0c 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -1,5 +1,6 @@ import { Logger } from '../../config/logger.config'; import { + AcceptGroupInvite, CreateGroupDto, GetParticipant, GroupDescriptionDto, @@ -65,6 +66,11 @@ export class GroupController { return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); } + public async acceptInviteCode(instance: InstanceDto, inviteCode: AcceptGroupInvite) { + logger.verbose('requested acceptInviteCode from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].acceptInviteCode(inviteCode); + } + public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index 6dfdc45c..293329d2 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -32,6 +32,10 @@ export class GroupInvite { inviteCode: string; } +export class AcceptGroupInvite { + inviteCode: string; +} + export class GroupSendInvite { groupJid: string; description: string; diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index f59e82a4..bf088129 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -2,6 +2,7 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { + AcceptGroupInviteSchema, createGroupSchema, getParticipantsSchema, groupInviteSchema, @@ -16,6 +17,7 @@ import { } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { + AcceptGroupInvite, CreateGroupDto, GetParticipant, GroupDescriptionDto, @@ -182,6 +184,22 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) + .get(this.routerPath('acceptInviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in acceptInviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.inviteCodeValidate({ + request: req, + schema: AcceptGroupInviteSchema, + ClassRef: AcceptGroupInvite, + execute: (instance, data) => groupController.acceptInviteCode(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { logger.verbose('request received in sendInvite'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index c49df6bf..024b88bd 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -89,6 +89,7 @@ import { WhatsAppNumberDto, } from '../dto/chat.dto'; import { + AcceptGroupInvite, CreateGroupDto, GetParticipant, GroupDescriptionDto, @@ -3744,6 +3745,16 @@ export class WAStartupService { } } + public async acceptInviteCode(id: AcceptGroupInvite) { + this.logger.verbose('Joining the group by invitation code: ' + id.inviteCode); + try { + const groupJid = await this.client.groupAcceptInvite(id.inviteCode); + return { accepted: true, groupJid: groupJid }; + } catch (error) { + throw new NotFoundException('Accept invite error', error.toString()); + } + } + public async revokeInviteCode(id: GroupJid) { this.logger.verbose('Revoking invite code for group: ' + id.groupJid); try { From 35520d85a26e74800550175981151634b49ebfbe Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Sun, 21 Jan 2024 13:00:23 -0300 Subject: [PATCH 042/144] Add https-proxy-agent dependency and handle NotFoundException in ProxyController --- package.json | 1 + src/whatsapp/controllers/proxy.controller.ts | 37 +++++++++++--------- src/whatsapp/whatsapp.module.ts | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index f0c56235..d5a5076f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "express": "^4.18.2", "express-async-errors": "^3.1.1", "hbs": "^4.2.0", + "https-proxy-agent": "^7.0.2", "jimp": "^0.16.13", "join": "^3.0.0", "js-yaml": "^4.1.0", diff --git a/src/whatsapp/controllers/proxy.controller.ts b/src/whatsapp/controllers/proxy.controller.ts index 555c5975..3613433b 100644 --- a/src/whatsapp/controllers/proxy.controller.ts +++ b/src/whatsapp/controllers/proxy.controller.ts @@ -1,19 +1,25 @@ import axios from 'axios'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../config/logger.config'; -import { BadRequestException } from '../../exceptions'; +import { BadRequestException, NotFoundException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { ProxyDto } from '../dto/proxy.dto'; +import { WAMonitoringService } from '../services/monitor.service'; import { ProxyService } from '../services/proxy.service'; const logger = new Logger('ProxyController'); export class ProxyController { - constructor(private readonly proxyService: ProxyService) {} + constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {} public async createProxy(instance: InstanceDto, data: ProxyDto) { logger.verbose('requested createProxy from ' + instance.instanceName + ' instance'); + if (!this.waMonitor.waInstances[instance.instanceName]) { + throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`); + } + if (!data.enabled) { logger.verbose('proxy disabled'); data.proxy = null; @@ -33,37 +39,36 @@ export class ProxyController { public async findProxy(instance: InstanceDto) { logger.verbose('requested findProxy from ' + instance.instanceName + ' instance'); + + if (!this.waMonitor.waInstances[instance.instanceName]) { + throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`); + } + 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, - }; + let proxyUrl = `${protocol}://${host}:${port}`; if (username && password) { - proxyConfig = { - ...proxyConfig, - auth: { - username: username, - password: password, - }, - }; + proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`; } const serverIp = await axios.get('http://meuip.com/api/meuip.php'); const response = await axios.get('http://meuip.com/api/meuip.php', { - proxy: proxyConfig, + httpsAgent: new HttpsProxyAgent(proxyUrl), }); logger.verbose('testProxy response: ' + response.data); return response.data !== serverIp.data; } catch (error) { - logger.error('testProxy error: ' + error); + let errorMessage = error; + if (axios.isAxiosError(error) && error.response.data) { + errorMessage = error.response.data; + } + logger.error('testProxy error: ' + errorMessage); return false; } } diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 0b5da554..3e52504f 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -119,7 +119,7 @@ export const websocketController = new WebsocketController(websocketService); const proxyService = new ProxyService(waMonitor); -export const proxyController = new ProxyController(proxyService); +export const proxyController = new ProxyController(proxyService, waMonitor); const chamaaiService = new ChamaaiService(waMonitor, configService); From 1e9b3a1e426ffc1784e83224fb5ce3d6af1405fb Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Sun, 21 Jan 2024 13:01:06 -0300 Subject: [PATCH 043/144] Update proxy controller to use a different IP lookup service --- src/whatsapp/controllers/proxy.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/whatsapp/controllers/proxy.controller.ts b/src/whatsapp/controllers/proxy.controller.ts index 3613433b..359b31a9 100644 --- a/src/whatsapp/controllers/proxy.controller.ts +++ b/src/whatsapp/controllers/proxy.controller.ts @@ -55,9 +55,9 @@ export class ProxyController { if (username && password) { proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`; } - const serverIp = await axios.get('http://meuip.com/api/meuip.php'); - const response = await axios.get('http://meuip.com/api/meuip.php', { + const serverIp = await axios.get('https://icanhazip.com/'); + const response = await axios.get('https://icanhazip.com/', { httpsAgent: new HttpsProxyAgent(proxyUrl), }); From 82e111f1be5646dabe468aee1cf1cf84d004db74 Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Sun, 21 Jan 2024 13:01:35 -0300 Subject: [PATCH 044/144] Fix proxy handling in WAStartupService preventing instance to be created --- src/whatsapp/services/whatsapp.service.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 80ac7397..d52b632a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1384,14 +1384,18 @@ export class WAStartupService { this.logger.info('Proxy enabled: ' + 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)); - const proxyUrl = 'http://' + proxyUrls[rand]; - options = { - agent: new ProxyAgent(proxyUrl as any), - }; + try { + 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)); + const proxyUrl = 'http://' + proxyUrls[rand]; + options = { + agent: new ProxyAgent(proxyUrl as any), + }; + } catch (error) { + this.localProxy.enabled = false; + } } else { let proxyUri = this.localProxy.proxy.protocol + '://' + this.localProxy.proxy.host + ':' + this.localProxy.proxy.port; From 5292e569d98166e7c8059189e649ab4cc17788e6 Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Sun, 21 Jan 2024 13:54:42 -0300 Subject: [PATCH 045/144] Remove unused dependencies and refactor proxy handling In some tests made on the testProxy function, the `new ProxyAgent()` was not working, even adding it the IP result was the same. I've change it to the `new HttpsProxyAgent()` and it worked. Refactored the proxy agent to center the same function/creation the same where it is used --- package.json | 1 - src/utils/makeProxyAgent.ts | 17 +++++++++++++++++ src/whatsapp/controllers/proxy.controller.ts | 15 ++++----------- src/whatsapp/services/whatsapp.service.ts | 17 +++++------------ 4 files changed, 26 insertions(+), 24 deletions(-) create mode 100644 src/utils/makeProxyAgent.ts diff --git a/package.json b/package.json index d5a5076f..5b36dc3d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "node-mime-types": "^1.1.0", "node-windows": "^1.0.0-beta.8", "pino": "^8.11.0", - "proxy-agent": "^6.3.0", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", "redis": "^4.6.5", diff --git a/src/utils/makeProxyAgent.ts b/src/utils/makeProxyAgent.ts new file mode 100644 index 00000000..45486523 --- /dev/null +++ b/src/utils/makeProxyAgent.ts @@ -0,0 +1,17 @@ +import { HttpsProxyAgent } from 'https-proxy-agent'; + +import { wa } from '../whatsapp/types/wa.types'; + +export function makeProxyAgent(proxy: wa.Proxy | string) { + if (typeof proxy === 'string') { + return new HttpsProxyAgent(proxy); + } + + const { host, password, port, protocol, username } = proxy; + let proxyUrl = `${protocol}://${host}:${port}`; + + if (username && password) { + proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`; + } + return new HttpsProxyAgent(proxyUrl); +} diff --git a/src/whatsapp/controllers/proxy.controller.ts b/src/whatsapp/controllers/proxy.controller.ts index 359b31a9..0dc79a3a 100644 --- a/src/whatsapp/controllers/proxy.controller.ts +++ b/src/whatsapp/controllers/proxy.controller.ts @@ -1,8 +1,8 @@ import axios from 'axios'; -import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../config/logger.config'; import { BadRequestException, NotFoundException } from '../../exceptions'; +import { makeProxyAgent } from '../../utils/makeProxyAgent'; import { InstanceDto } from '../dto/instance.dto'; import { ProxyDto } from '../dto/proxy.dto'; import { WAMonitoringService } from '../services/monitor.service'; @@ -27,8 +27,7 @@ export class ProxyController { 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); + const testProxy = await this.testProxy(data.proxy); if (!testProxy) { throw new BadRequestException('Invalid proxy'); } @@ -47,18 +46,12 @@ export class ProxyController { return this.proxyService.find(instance); } - private async testProxy(host: string, port: string, protocol: string, username?: string, password?: string) { + private async testProxy(proxy: ProxyDto['proxy']) { logger.verbose('requested testProxy'); try { - let proxyUrl = `${protocol}://${host}:${port}`; - - if (username && password) { - proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`; - } - const serverIp = await axios.get('https://icanhazip.com/'); const response = await axios.get('https://icanhazip.com/', { - httpsAgent: new HttpsProxyAgent(proxyUrl), + httpsAgent: makeProxyAgent(proxy), }); logger.verbose('testProxy response: ' + response.data); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index d52b632a..9b9539ce 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -45,7 +45,6 @@ import { getMIMEType } from 'node-mime-types'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; -import { ProxyAgent } from 'proxy-agent'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; @@ -73,6 +72,7 @@ import { dbserver } from '../../libs/db.connect'; import { RedisCache } from '../../libs/redis.client'; import { getIO } from '../../libs/socket.server'; import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; +import { makeProxyAgent } from '../../utils/makeProxyAgent'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { @@ -1391,21 +1391,14 @@ export class WAStartupService { const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const proxyUrl = 'http://' + proxyUrls[rand]; options = { - agent: new ProxyAgent(proxyUrl as any), + agent: makeProxyAgent(proxyUrl), }; } catch (error) { this.localProxy.enabled = false; } } 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(proxyUri as any), + agent: makeProxyAgent(this.localProxy.proxy), }; } } @@ -1492,8 +1485,8 @@ export class WAStartupService { if (this.localProxy.enabled) { this.logger.verbose('Proxy enabled'); options = { - agent: new ProxyAgent(this.localProxy.proxy as any), - fetchAgent: new ProxyAgent(this.localProxy.proxy as any), + agent: makeProxyAgent(this.localProxy.proxy), + fetchAgent: makeProxyAgent(this.localProxy.proxy), }; } From 9e5bf935809f53ab9654687864b244457625f5df Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Mon, 22 Jan 2024 13:39:11 -0300 Subject: [PATCH 046/144] Fix polls in message sending --- src/whatsapp/services/whatsapp.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 0d8fb5b2..df455a06 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2469,7 +2469,7 @@ export class WAStartupService { ); } - if (!message['audio'] && sender != 'status@broadcast') { + if (!message['audio'] && !message['poll'] && sender != 'status@broadcast') { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, From ed6c50621c97c1e587aa95bdd3ed805aeed5c48d Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Tue, 23 Jan 2024 18:16:15 -0300 Subject: [PATCH 047/144] Improved the method of numbers validation --- package.json | 2 + src/whatsapp/services/whatsapp.service.ts | 73 ++++++++++++++++------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 20ff59d9..a1477452 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "exiftool-vendored": "^22.0.0", "express": "^4.18.2", "express-async-errors": "^3.1.1", + "fast-levenshtein": "^3.0.0", "hbs": "^4.2.0", "https-proxy-agent": "^7.0.2", "jimp": "^0.16.13", @@ -88,6 +89,7 @@ "@types/compression": "^1.7.2", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", + "@types/fast-levenshtein": "^0.0.4", "@types/js-yaml": "^4.0.5", "@types/jsonwebtoken": "^8.5.9", "@types/mime-types": "^2.1.1", diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 0d8fb5b2..76511260 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -38,6 +38,7 @@ import axios from 'axios'; import { exec, execSync } from 'child_process'; import { arrayUnique, isBase64, isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; +import levenshtein from 'fast-levenshtein'; import fs, { existsSync, readFileSync } from 'fs'; import Long from 'long'; import NodeCache from 'node-cache'; @@ -3126,31 +3127,63 @@ export class WAStartupService { public async whatsappNumber(data: WhatsAppNumberDto) { this.logger.verbose('Getting whatsapp number'); - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); + const jids: { + groups: { number: string; jid: string }[]; + broadcast: { number: string; jid: string }[]; + users: { number: string; jid: string }[]; + } = { + groups: [], + broadcast: [], + users: [], + }; + + data.numbers.forEach((number) => { + const jid = this.createJid(number); if (isJidGroup(jid)) { + jids.groups.push({ number, jid }); + } else if (jid === 'status@broadcast') { + jids.broadcast.push({ number, jid }); + } else { + jids.users.push({ number, jid }); + } + }); + + const onWhatsapp: OnWhatsAppDto[] = []; + + // BROADCAST + onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number))); + + // GROUPS + const groups = await Promise.all( + jids.groups.map(async ({ jid, number }) => { const group = await this.findGroup({ groupJid: jid }, 'inner'); - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject)); - } else if (jid === 'status@broadcast') { - onWhatsapp.push(new OnWhatsAppDto(jid, false, number)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false, number)); - } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists, number)); + if (!group) { + new OnWhatsAppDto(jid, false, number); } - } - } + + return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject); + }), + ); + onWhatsapp.push(...groups); + + // USERS + const verify = await this.client.onWhatsApp( + ...jids.users.map(({ jid }) => (!jid.startsWith('+') ? `+${jid}` : jid)), + ); + const users: OnWhatsAppDto[] = jids.users.map((user) => { + const MAX_SIMILARITY_THRESHOLD = 0.0358; + const numberVerified = verify.find( + (v) => levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length) <= MAX_SIMILARITY_THRESHOLD, + ); + return { + exists: !!numberVerified?.exists, + jid: numberVerified?.jid || user.jid, + number: user.number, + }; + }); + onWhatsapp.push(...users); return onWhatsapp; } From eb96c9feceeba6a24a358244494aa0ea8e255d45 Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Tue, 23 Jan 2024 18:37:31 -0300 Subject: [PATCH 048/144] Added new validation on the 9 digit This is to prevent returning another jid when only one digit is different and not the 9 digit --- src/whatsapp/services/whatsapp.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 76511260..4919c1bb 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3173,10 +3173,15 @@ export class WAStartupService { ...jids.users.map(({ jid }) => (!jid.startsWith('+') ? `+${jid}` : jid)), ); const users: OnWhatsAppDto[] = jids.users.map((user) => { - const MAX_SIMILARITY_THRESHOLD = 0.0358; - const numberVerified = verify.find( - (v) => levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length) <= MAX_SIMILARITY_THRESHOLD, - ); + const MAX_SIMILARITY_THRESHOLD = 0.01; + const isBrWithDigit = user.jid.startsWith('55') && user.jid.slice(4, 5) === '9' && user.jid.length === 28; + const jid = isBrWithDigit ? user.jid.slice(0, 4) + user.jid.slice(5) : user.jid; + + const numberVerified = verify.find((v) => { + const mainJidSimilarity = levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length); + const jidSimilarity = levenshtein.get(jid, v.jid) / Math.max(jid.length, v.jid.length); + return mainJidSimilarity <= MAX_SIMILARITY_THRESHOLD || jidSimilarity <= MAX_SIMILARITY_THRESHOLD; + }); return { exists: !!numberVerified?.exists, jid: numberVerified?.jid || user.jid, From 804d1777824e530ce1a15d5a7fbb7acaeff73ad0 Mon Sep 17 00:00:00 2001 From: Amilton Morais Date: Tue, 23 Jan 2024 19:34:36 -0300 Subject: [PATCH 049/144] Udate "@whiskeysockets/baileys": "6.6.0" and models The message.model.ts class was updated to version 6.6.0 of baileys. 'unknown','desktop' was included to avoid errors using version 6.6.0 of baileys --- package.json | 12 +++++++----- src/whatsapp/models/message.model.ts | 4 ++-- start.sh | 0 3 files changed, 9 insertions(+), 7 deletions(-) mode change 100755 => 100644 start.sh diff --git a/package.json b/package.json index 20ff59d9..4fbf620b 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,11 @@ "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@sentry/node": "^7.59.2", - "@whiskeysockets/baileys": "6.5.0", + "@whiskeysockets/baileys": "6.6.0", "amqplib": "^0.10.3", "aws-sdk": "^2.1499.0", - "axios": "^1.3.5", - "class-validator": "^0.13.2", + "axios": "^1.6.5", + "class-validator": "^0.14.1", "compression": "^1.7.4", "cors": "^2.8.5", "cross-env": "^7.0.3", @@ -66,22 +66,24 @@ "join": "^3.0.0", "js-yaml": "^4.1.0", "jsonschema": "^1.4.1", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.2", "libphonenumber-js": "^1.10.39", "link-preview-js": "^3.0.4", "mongoose": "^6.10.5", "node-cache": "^5.1.2", "node-mime-types": "^1.1.0", "node-windows": "^1.0.0-beta.8", + "parse-bmfont-xml": "^1.1.4", "pino": "^8.11.0", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", "redis": "^4.6.5", - "sharp": "^0.30.7", + "sharp": "^0.32.2", "socket.io": "^4.7.1", "socks-proxy-agent": "^8.0.1", "swagger-ui-express": "^5.0.0", "uuid": "^9.0.0", + "xml2js": "^0.6.2", "yamljs": "^0.3.0" }, "devDependencies": { diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 9c7ac9dc..8388799c 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -26,7 +26,7 @@ export class MessageRaw { messageType?: string; messageTimestamp?: number | Long.Long; owner: string; - source?: 'android' | 'web' | 'ios'; + source?: 'android' | 'web' | 'ios' | 'unknown' | 'desktop'; source_id?: string; source_reply_id?: string; chatwoot?: ChatwootMessage; @@ -45,7 +45,7 @@ const messageSchema = new Schema({ participant: { type: String }, messageType: { type: String }, message: { type: Object }, - source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, + source: { type: String, minlength: 3, enum: ['android', 'web', 'ios','unknown','desktop' ] }, messageTimestamp: { type: Number, required: true }, owner: { type: String, required: true, minlength: 1 }, chatwoot: { diff --git a/start.sh b/start.sh old mode 100755 new mode 100644 From 679f8118f6fc16e806dbeb13ec12f6743644ffa7 Mon Sep 17 00:00:00 2001 From: Judson Junior Date: Tue, 23 Jan 2024 23:59:12 -0300 Subject: [PATCH 050/144] Added sendList endpoint to swagger documentation --- src/docs/swagger.yaml | 67 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 924434e9..d3301cae 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -940,7 +940,72 @@ paths: description: Successful response content: application/json: {} - + /message/sendList/{instanceName}: + post: + tags: + - Send Message Controller + summary: Send a list to a specified instance. + description: This endpoint allows users to send a list to a chat. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + number: + type: string + options: + type: object + properties: + delay: + type: integer + presence: + type: string + listMessage: + type: object + properties: + title: + type: string + description: + type: string + footerText: + type: string + nullable: true + buttonText: + type: string + sections: + type: array + items: + type: object + properties: + title: + type: string + rows: + type: array + items: + type: object + properties: + title: + type: string + description: + type: string + rowId: + type: string + parameters: + - name: instanceName + in: path + required: true + schema: + type: string + description: The name of the instance to which the poll should be sent. + example: "evolution" + responses: + "200": + description: Successful response + content: + application/json: {} + /chat/whatsappNumbers/{instanceName}: post: tags: From 0e50da324b9b0c8f3effa6206a2007fff116599b Mon Sep 17 00:00:00 2001 From: Amilton Morais Date: Wed, 24 Jan 2024 13:38:29 -0300 Subject: [PATCH 051/144] Implemented a function to synchronize message deletions on WhatsApp, automatically reflecting in Chatwoot A new variable has been created, and a function has been implemented to manage the deletion of messages on WhatsApp. Now, when deleting a message for everyone on WhatsApp, the same action will be automatically performed on Chatwoot, ensuring consistency across platforms --- src/config/env.config.ts | 5 +++ src/dev-env.yml | 4 ++ src/whatsapp/models/message.model.ts | 4 +- src/whatsapp/services/chatwoot.service.ts | 54 ++++++++++++----------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index fde4a073..25dd72a3 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -147,6 +147,7 @@ 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 ChatWoot = { MESSAGE_DELETE: boolean }; export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type Production = boolean; @@ -167,6 +168,7 @@ export interface Env { CONFIG_SESSION_PHONE: ConfigSessionPhone; QRCODE: QrCode; TYPEBOT: Typebot; + CHATWOOT: ChatWoot; CACHE: CacheConf; AUTHENTICATION: Auth; PRODUCTION?: Production; @@ -330,6 +332,9 @@ export class ConfigService { API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true', }, + CHATWOOT: { + MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'false', + }, CACHE: { REDIS: { ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', diff --git a/src/dev-env.yml b/src/dev-env.yml index 42438aff..c4a9d909 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -153,6 +153,10 @@ TYPEBOT: API_VERSION: 'old' # old | latest KEEP_OPEN: false +# If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot. +CHATWOOT: + MESSAGE_DELETE: true # false | true + # Cache to optimize application performance CACHE: REDIS: diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 9c7ac9dc..4deb0f0f 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -26,7 +26,7 @@ export class MessageRaw { messageType?: string; messageTimestamp?: number | Long.Long; owner: string; - source?: 'android' | 'web' | 'ios'; + source?: 'android' | 'web' | 'ios' | 'ios' | 'unknown' | 'desktop'; source_id?: string; source_reply_id?: string; chatwoot?: ChatwootMessage; @@ -45,7 +45,7 @@ const messageSchema = new Schema({ participant: { type: String }, messageType: { type: String }, message: { type: Object }, - source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, + source: { type: String, minlength: 3, enum: ['android', 'web', 'ios','unknown','desktop' ] }, messageTimestamp: { type: Number, required: true }, owner: { type: String, required: true, minlength: 1 }, chatwoot: { diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index ef70cf63..3c4c011c 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -7,7 +7,7 @@ import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; -import { ConfigService, HttpServer } from '../../config/env.config'; +import { ConfigService, HttpServer, ChatWoot} from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { ICache } from '../abstract/abstract.cache'; import { ChatwootDto } from '../dto/chatwoot.dto'; @@ -1894,33 +1894,37 @@ export class ChatwootService { } if (event === Events.MESSAGES_DELETE) { - this.logger.verbose('deleting message from instance: ' + instance.instanceName); + + const chatwootDelete = this.configService.get('CHATWOOT').MESSAGE_DELETE + if (chatwootDelete === true) { + this.logger.verbose('deleting message from instance: ' + instance.instanceName); - if (!body?.key?.id) { - this.logger.warn('message id not found'); - return; - } + 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 repository. Message id: ' + body.key.id); - this.repository.message.delete({ - where: { - key: { - id: body.key.id, - }, - owner: instance.instanceName, - }, - }); + 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, - conversationId: message.chatwoot.conversationId, - messageId: message.chatwoot.messageId, - }); - } - } + 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 === 'messages.read') { this.logger.verbose('read message from instance: ' + instance.instanceName); From dfa72fd6af03160af609378b29df78e86e24b5a4 Mon Sep 17 00:00:00 2001 From: Amilton Morais Date: Wed, 24 Jan 2024 13:43:19 -0300 Subject: [PATCH 052/144] Update start.sh --- start.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 start.sh diff --git a/start.sh b/start.sh old mode 100755 new mode 100644 From 0edc8a92842441e012a1e58baf08d4742ecbdd66 Mon Sep 17 00:00:00 2001 From: Douglas Rauber at Nitro Date: Thu, 25 Jan 2024 16:19:08 -0300 Subject: [PATCH 053/144] Add translate capabilities to QRMessages in CW --- .gitignore | 3 +- package.json | 1 + src/config/env.config.ts | 3 ++ src/dev-env.yml | 3 ++ src/utils/i18n.ts | 36 +++++++++++++++++++++++ src/utils/translations/en.json | 5 ++++ src/utils/translations/pt-BR.json | 5 ++++ src/whatsapp/services/chatwoot.service.ts | 8 +++-- src/whatsapp/services/typebot.service.ts | 10 +++++-- 9 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/utils/i18n.ts create mode 100644 src/utils/translations/en.json create mode 100644 src/utils/translations/pt-BR.json diff --git a/.gitignore b/.gitignore index c55eb628..55cd9d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ docker-compose.yaml /temp/* .DS_Store -*.DS_Store \ No newline at end of file +*.DS_Store +.tool-versions diff --git a/package.json b/package.json index 20ff59d9..3f2cf39f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "express-async-errors": "^3.1.1", "hbs": "^4.2.0", "https-proxy-agent": "^7.0.2", + "i18next": "^23.7.19", "jimp": "^0.16.13", "join": "^3.0.0", "js-yaml": "^4.1.0", diff --git a/src/config/env.config.ts b/src/config/env.config.ts index fde4a073..cfe525d7 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -127,6 +127,8 @@ export type Auth = { export type DelInstance = number | boolean; +export type Language = string | 'en'; + export type GlobalWebhook = { URL: string; ENABLED: boolean; @@ -163,6 +165,7 @@ export interface Env { WEBSOCKET: Websocket; LOG: Log; DEL_INSTANCE: DelInstance; + LANGUAGE: Language; WEBHOOK: Webhook; CONFIG_SESSION_PHONE: ConfigSessionPhone; QRCODE: QrCode; diff --git a/src/dev-env.yml b/src/dev-env.yml index 42438aff..6c9aa372 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -179,3 +179,6 @@ AUTHENTICATION: JWT: EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires SECRET: L=0YWt]b2w[WF>#>:&E` + + +LANGUAGE: "pt-BR" # pt-BR, en \ No newline at end of file diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 00000000..65feab48 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,36 @@ +import fs from 'fs'; +import i18next from 'i18next'; +import path from 'path'; + +import { ConfigService, Language } from '../config/env.config'; + +// export class i18n { +// constructor(private readonly configService: ConfigService) { +const languages = ['en', 'pt-BR']; +const translationsPath = path.join(__dirname, 'translations'); +const configService: ConfigService = new ConfigService(); + +const resources: any = {}; + +languages.forEach((language) => { + const languagePath = path.join(translationsPath, `${language}.json`); + if (fs.existsSync(languagePath)) { + resources[language] = { + translation: require(languagePath), + }; + } +}); + +i18next.init({ + resources, + fallbackLng: 'en', + lng: configService.get('LANGUAGE'), + debug: false, + + interpolation: { + escapeValue: false, + }, +}); +// } +// } +export default i18next; diff --git a/src/utils/translations/en.json b/src/utils/translations/en.json new file mode 100644 index 00000000..f92c4e08 --- /dev/null +++ b/src/utils/translations/en.json @@ -0,0 +1,5 @@ +{ + "qrgeneratedsuccesfully": "QRCode successfully generated!", + "scanqr": "Scan this QR code within the next 40 seconds.", + "qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again." +} \ No newline at end of file diff --git a/src/utils/translations/pt-BR.json b/src/utils/translations/pt-BR.json new file mode 100644 index 00000000..d48ff148 --- /dev/null +++ b/src/utils/translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "qrgeneratedsuccesfully": "QRCode gerado com sucesso!", + "scanqr": "Escanei o QRCode com o Whatsapp nos próximos 40 segundos.", + "qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa." +} \ No newline at end of file diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index ef70cf63..b9ab9c3f 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -9,6 +9,7 @@ import path from 'path'; import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import i18next from '../../utils/i18n'; import { ICache } from '../abstract/abstract.cache'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; @@ -1994,7 +1995,8 @@ export class ChatwootService { 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.`; + + const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`; this.logger.verbose('send message to chatwoot'); return await this.createBotMessage(instance, erroQRcode, 'incoming'); @@ -2010,9 +2012,9 @@ export class ChatwootService { writeFileSync(fileName, fileData, 'utf8'); this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); + await this.createBotQr(instance, i18next.t('qrgeneratedsuccesfully'), 'incoming', fileName); - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`; if (body?.qrcode?.pairingCode) { msgQrCode = diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index 36645f63..85eb3558 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -274,7 +274,7 @@ export class TypebotService { const types = { conversation: msg.conversation, extendedTextMessage: msg.extendedTextMessage?.text, - responseRowId: msg.listResponseMessage.singleSelectReply?.selectedRowId, + responseRowId: msg.listResponseMessage?.singleSelectReply?.selectedRowId, }; this.logger.verbose('type message: ' + types); @@ -413,7 +413,13 @@ export class TypebotService { text += element.text; } - if (element.children && (element.type === 'p' || element.type === 'a' || element.type === 'inline-variable' || element.type === 'variable')) { + if ( + element.children && + (element.type === 'p' || + element.type === 'a' || + element.type === 'inline-variable' || + element.type === 'variable') + ) { for (const child of element.children) { text += applyFormatting(child); } From 3755d3870e7aea209137f74cf76f48e8dce2b4f9 Mon Sep 17 00:00:00 2001 From: edisoncm-ti Date: Fri, 26 Jan 2024 19:10:20 -0300 Subject: [PATCH 054/144] fix: collecting responses with text or numbers in Typebot --- src/whatsapp/services/typebot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index 36645f63..6d6237f9 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -274,7 +274,7 @@ export class TypebotService { const types = { conversation: msg.conversation, extendedTextMessage: msg.extendedTextMessage?.text, - responseRowId: msg.listResponseMessage.singleSelectReply?.selectedRowId, + responseRowId: msg.listResponseMessage?.singleSelectReply?.selectedRowId, }; this.logger.verbose('type message: ' + types); From 59f5208c5c05526e08a210e7776b58653e6d8a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Cl=C3=AAuder=20Lima=20de=20Jesus?= Date: Sat, 27 Jan 2024 22:29:04 -0300 Subject: [PATCH 055/144] fix: search number without 9 in number from brazil --- src/whatsapp/services/chatwoot.service.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 26b0cce9..c78d2bb1 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -380,8 +380,9 @@ export class ChatwootService { } let query: any; + const isGroup = phoneNumber.includes('@g.us'); - if (!phoneNumber.includes('@g.us')) { + if (!isGroup) { this.logger.verbose('format phone number'); query = `+${phoneNumber}`; } else { @@ -390,12 +391,21 @@ export class ChatwootService { } this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ + let contact: any = await client.contacts.search({ accountId: this.provider.account_id, q: query, }); - if (!contact) { + if (!contact && !isGroup && query.startsWith('+55') && query.length > 13) { + this.logger.verbose('trying without the 9th digit'); + query = query.slice(0, 3) + query.slice(4); + contact = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); + } + + if(!contact) { this.logger.warn('contact not found'); return null; } From 838905f3dde77b8449cfe0d27660f60ba9d9562b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Cl=C3=AAuder=20Lima=20de=20Jesus?= Date: Sat, 27 Jan 2024 22:37:58 -0300 Subject: [PATCH 056/144] fix: position splice --- 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 c78d2bb1..e4754cd8 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -398,7 +398,7 @@ export class ChatwootService { if (!contact && !isGroup && query.startsWith('+55') && query.length > 13) { this.logger.verbose('trying without the 9th digit'); - query = query.slice(0, 3) + query.slice(4); + query = query.slice(0, 5) + query.slice(6); contact = await client.contacts.search({ accountId: this.provider.account_id, q: query, From 535d5ee47f50d1ea743ef5d409fbf58dc05860f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yves=20Cl=C3=AAuder=20Lima=20de=20Jesus?= Date: Sun, 28 Jan 2024 10:36:42 -0300 Subject: [PATCH 057/144] fix: change method search for filter --- src/whatsapp/services/chatwoot.service.ts | 70 ++++++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index e4754cd8..90e2ad6c 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -391,18 +391,18 @@ export class ChatwootService { } this.logger.verbose('find contact in chatwoot'); - let contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); + let contact: any; - if (!contact && !isGroup && query.startsWith('+55') && query.length > 13) { - this.logger.verbose('trying without the 9th digit'); - query = query.slice(0, 5) + query.slice(6); + if(isGroup) { contact = await client.contacts.search({ accountId: this.provider.account_id, q: query, }); + } else { + contact = await client.contacts.filter({ + accountId: this.provider.account_id, + payload: this.getFilterPayload(query), + }); } if(!contact) { @@ -410,15 +410,67 @@ export class ChatwootService { return null; } - if (!phoneNumber.includes('@g.us')) { + if (!isGroup) { this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); + return this.findContactInContactList(contact.payload, query); } else { this.logger.verbose('return group'); return contact.payload.find((contact) => contact.identifier === query); } } + private findContactInContactList(contacts: any[], query: string) { + const phoneNumbers = this.getNumbers(query); + const searchableFields = this.getSearchableFields(); + + for (const contact of contacts) { + for (const field of searchableFields) { + if (contact[field] && phoneNumbers.includes(contact[field])) { + return contact; + } + } + } + + return null; + } + + private getNumbers(query: string) { + const numbers = []; + numbers.push(query); + + if (query.startsWith('+55') && query.length === 14) { + const withoutNine = query.slice(0, 5) + query.slice(6); + numbers.push(withoutNine); + } else if (query.startsWith('+55') && query.length === 13) { + const withNine = query.slice(0, 5) + '9' + query.slice(5); + numbers.push(withNine); + } + + return numbers; + } + + private getSearchableFields() { + return ['identifier', 'phone_number', 'name', 'email']; + } + + private getFilterPayload(query: string) { + const payload = []; + const values = this.getNumbers(query) + + const fields = this.getSearchableFields(); + fields.forEach((key, index) => { + const queryOperator = fields.length - 1 === index ? null : 'OR'; + payload.push({ + "attribute_key": key, + "filter_operator": "contains", + "values": values, + "query_operator": queryOperator + }); + }); + + return payload; + } + public async createConversation(instance: InstanceDto, body: any) { this.logger.verbose('create conversation to instance: ' + instance.instanceName); try { From cdf822291f6c50d4165f0e53ac53ad5e6ff6bf33 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 29 Jan 2024 11:35:30 -0300 Subject: [PATCH 058/144] lint --- src/config/env.config.ts | 2 +- src/whatsapp/models/message.model.ts | 2 +- src/whatsapp/services/chatwoot.service.ts | 57 +++++++++++------------ 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index fcc00c7c..fec66eff 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -149,7 +149,7 @@ 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 ChatWoot = { MESSAGE_DELETE: boolean }; +export type ChatWoot = { MESSAGE_DELETE: boolean }; export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type Production = boolean; diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 8388799c..326f982b 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -45,7 +45,7 @@ const messageSchema = new Schema({ participant: { type: String }, messageType: { type: String }, message: { type: Object }, - source: { type: String, minlength: 3, enum: ['android', 'web', 'ios','unknown','desktop' ] }, + source: { type: String, minlength: 3, enum: ['android', 'web', 'ios', 'unknown', 'desktop'] }, messageTimestamp: { type: Number, required: true }, owner: { type: String, required: true, minlength: 1 }, chatwoot: { diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index d0c0ac85..afa6e874 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -7,7 +7,7 @@ import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; -import { ConfigService, HttpServer, ChatWoot} from '../../config/env.config'; +import { ChatWoot, ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import i18next from '../../utils/i18n'; import { ICache } from '../abstract/abstract.cache'; @@ -1895,37 +1895,36 @@ export class ChatwootService { } if (event === Events.MESSAGES_DELETE) { - - const chatwootDelete = this.configService.get('CHATWOOT').MESSAGE_DELETE - if (chatwootDelete === true) { - this.logger.verbose('deleting message from instance: ' + instance.instanceName); + const chatwootDelete = this.configService.get('CHATWOOT').MESSAGE_DELETE; + if (chatwootDelete === true) { + this.logger.verbose('deleting message from instance: ' + instance.instanceName); - if (!body?.key?.id) { - this.logger.warn('message id not found'); - return; - } + 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 repository. Message id: ' + body.key.id); - this.repository.message.delete({ - where: { - key: { - id: body.key.id, - }, - owner: instance.instanceName, - }, - }); + 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, - conversationId: message.chatwoot.conversationId, - messageId: message.chatwoot.messageId, - }); - } - } - } + 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 === 'messages.read') { this.logger.verbose('read message from instance: ' + instance.instanceName); From 058acc5042be96e42c2329b397c385c1833c6a25 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 29 Jan 2024 11:43:42 -0300 Subject: [PATCH 059/144] changelog --- CHANGELOG.md | 15 +++++++++++++++ src/config/env.config.ts | 1 + 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9033f10f..a9284df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ### Feature * Added update message endpoint +* Add translate capabilities to QRMessages in CW +* Join in Group by Invite Code +* Read messages from whatsapp in chatwoot +* Add support to use use redis in cacheservice ### Fixed @@ -16,6 +20,17 @@ * 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 +* Collecting responses with text or numbers in Typebot +* Added sendList endpoint to swagger documentation +* Implemented a function to synchronize message deletions on WhatsApp, automatically reflecting in Chatwoot. +* Improvement on numbers validation +* Fix polls in message sending +* Sending status message +* Message 'connection successfully' spamming +* Invalidate the conversation cache if reopen_conversation is false and the conversation was resolved +* Fix looping when deleting a message in chatwoot +* When receiving a file from whatsapp, use the original filename in chatwoot if possible +* Correction in the sendList Function # 1.6.1 (2023-12-22 11:43) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index fec66eff..fd6187ab 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -286,6 +286,7 @@ export class ConfigService { DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) ? process.env.DEL_INSTANCE === 'true' : Number.parseInt(process.env.DEL_INSTANCE) || false, + LANGUAGE: process.env?.LANGUAGE || 'en', WEBHOOK: { GLOBAL: { URL: process.env?.WEBHOOK_GLOBAL_URL || '', From 66b82ac10a89c1a740a05332e2017d99c329b9ff Mon Sep 17 00:00:00 2001 From: Leandro Rocha Date: Wed, 31 Jan 2024 10:50:04 -0300 Subject: [PATCH 060/144] If contact stored with name, name added in return --- src/whatsapp/services/whatsapp.service.ts | 51 +++++++++++++++-------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 15cf7176..baaba15d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3130,7 +3130,7 @@ export class WAStartupService { const jids: { groups: { number: string; jid: string }[]; broadcast: { number: string; jid: string }[]; - users: { number: string; jid: string }[]; + users: { number: string; jid: string; contact?: string; name?: string }[]; } = { groups: [], broadcast: [], @@ -3145,7 +3145,7 @@ export class WAStartupService { } else if (jid === 'status@broadcast') { jids.broadcast.push({ number, jid }); } else { - jids.users.push({ number, jid }); + jids.users.push({ number, jid, contact: jid }); } }); @@ -3172,22 +3172,39 @@ export class WAStartupService { const verify = await this.client.onWhatsApp( ...jids.users.map(({ jid }) => (!jid.startsWith('+') ? `+${jid}` : jid)), ); - const users: OnWhatsAppDto[] = jids.users.map((user) => { - const MAX_SIMILARITY_THRESHOLD = 0.01; - const isBrWithDigit = user.jid.startsWith('55') && user.jid.slice(4, 5) === '9' && user.jid.length === 28; - const jid = isBrWithDigit ? user.jid.slice(0, 4) + user.jid.slice(5) : user.jid; + const users: OnWhatsAppDto[] = await Promise.all( + jids.users.map(async (user) => { + const MAX_SIMILARITY_THRESHOLD = 0.01; + const isBrWithDigit = user.jid.startsWith('55') && user.jid.slice(4, 5) === '9' && user.jid.length === 28; + const jid = isBrWithDigit ? user.jid.slice(0, 4) + user.jid.slice(5) : user.jid; + + const query: ContactQuery = { + where: { + owner: this.instance.name, + id: user.contact, + }, + }; + const contacts: ContactRaw[] = await this.repository.contact.find(query); + let firstContactFound; + if (contacts.length > 0) { + firstContactFound = contacts[0].pushName; + console.log(contacts[0]); + } + + const numberVerified = verify.find((v) => { + const mainJidSimilarity = levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length); + const jidSimilarity = levenshtein.get(jid, v.jid) / Math.max(jid.length, v.jid.length); + return mainJidSimilarity <= MAX_SIMILARITY_THRESHOLD || jidSimilarity <= MAX_SIMILARITY_THRESHOLD; + }); + return { + exists: !!numberVerified?.exists, + jid: numberVerified?.jid || user.jid, + name: firstContactFound, + number: user.number, + }; + }), + ); - const numberVerified = verify.find((v) => { - const mainJidSimilarity = levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length); - const jidSimilarity = levenshtein.get(jid, v.jid) / Math.max(jid.length, v.jid.length); - return mainJidSimilarity <= MAX_SIMILARITY_THRESHOLD || jidSimilarity <= MAX_SIMILARITY_THRESHOLD; - }); - return { - exists: !!numberVerified?.exists, - jid: numberVerified?.jid || user.jid, - number: user.number, - }; - }); onWhatsapp.push(...users); return onWhatsapp; From 3da73b821d2f71bdfad932000943d19fbed94f06 Mon Sep 17 00:00:00 2001 From: Leandro Santos Rocha Date: Wed, 31 Jan 2024 10:55:37 -0300 Subject: [PATCH 061/144] Removed console.log --- src/whatsapp/services/whatsapp.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index baaba15d..77d5570d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3188,7 +3188,6 @@ export class WAStartupService { let firstContactFound; if (contacts.length > 0) { firstContactFound = contacts[0].pushName; - console.log(contacts[0]); } const numberVerified = verify.find((v) => { From dfb1ee0c568aa12511c8ec818fdb7f82ed4007f3 Mon Sep 17 00:00:00 2001 From: Leandro Santos Rocha Date: Wed, 31 Jan 2024 11:37:19 -0300 Subject: [PATCH 062/144] Adjust to use the same jid --- src/whatsapp/services/whatsapp.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 77d5570d..4858e433 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3130,7 +3130,7 @@ export class WAStartupService { const jids: { groups: { number: string; jid: string }[]; broadcast: { number: string; jid: string }[]; - users: { number: string; jid: string; contact?: string; name?: string }[]; + users: { number: string; jid: string; name?: string }[]; } = { groups: [], broadcast: [], @@ -3145,7 +3145,7 @@ export class WAStartupService { } else if (jid === 'status@broadcast') { jids.broadcast.push({ number, jid }); } else { - jids.users.push({ number, jid, contact: jid }); + jids.users.push({ number, jid }); } }); @@ -3181,7 +3181,7 @@ export class WAStartupService { const query: ContactQuery = { where: { owner: this.instance.name, - id: user.contact, + id: user.jid.startsWith('+') ? user.jid.substring(1) : user.jid;, }, }; const contacts: ContactRaw[] = await this.repository.contact.find(query); From 7439d2401de1a75cfbf5ea94376974bbe9e853cd Mon Sep 17 00:00:00 2001 From: Leandro Santos Rocha Date: Wed, 31 Jan 2024 11:39:04 -0300 Subject: [PATCH 063/144] Fix error ; --- src/whatsapp/services/whatsapp.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 4858e433..71ca431e 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -3181,7 +3181,7 @@ export class WAStartupService { const query: ContactQuery = { where: { owner: this.instance.name, - id: user.jid.startsWith('+') ? user.jid.substring(1) : user.jid;, + id: user.jid.startsWith('+') ? user.jid.substring(1) : user.jid, }, }; const contacts: ContactRaw[] = await this.repository.contact.find(query); From f41f3aaba8d85281c0a3c3cef0e17e894a5f55fc Mon Sep 17 00:00:00 2001 From: jaison-x Date: Thu, 1 Feb 2024 17:31:50 -0300 Subject: [PATCH 064/144] Update chatwoot.service.ts hotfix: bug on chatwoot sdk --- src/whatsapp/services/chatwoot.service.ts | 28 +++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 84028cf6..0bc74ecd 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -382,19 +382,27 @@ export class ChatwootService { this.logger.verbose('find contact in chatwoot'); let contact: any; - if(isGroup) { + if (isGroup) { contact = await client.contacts.search({ accountId: this.provider.account_id, q: query, }); } else { - contact = await client.contacts.filter({ - accountId: this.provider.account_id, - payload: this.getFilterPayload(query), + // contact = await client.contacts.filter({ + // accountId: this.provider.account_id, + // payload: this.getFilterPayload(query), + // }); + // hotfix for: https://github.com/EvolutionAPI/evolution-api/pull/382. waiting fix: https://github.com/figurolatam/chatwoot-sdk/pull/7 + contact = await chatwootRequest(this.getClientCwConfig(), { + method: 'POST', + url: `/api/v1/accounts/${this.provider.account_id}/contacts/filter`, + body: { + payload: this.getFilterPayload(query), + }, }); } - if(!contact) { + if (!contact) { this.logger.warn('contact not found'); return null; } @@ -444,16 +452,16 @@ export class ChatwootService { private getFilterPayload(query: string) { const payload = []; - const values = this.getNumbers(query) + const values = this.getNumbers(query); const fields = this.getSearchableFields(); fields.forEach((key, index) => { const queryOperator = fields.length - 1 === index ? null : 'OR'; payload.push({ - "attribute_key": key, - "filter_operator": "contains", - "values": values, - "query_operator": queryOperator + attribute_key: key, + filter_operator: 'contains', + values: values, + query_operator: queryOperator, }); }); From b995cdfc32c601623adff8555e386856291322bd Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Thu, 1 Feb 2024 18:53:36 -0300 Subject: [PATCH 065/144] Fix for contats find payload --- src/whatsapp/services/chatwoot.service.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0bc74ecd..729dcb31 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -455,19 +455,22 @@ export class ChatwootService { const values = this.getNumbers(query); const fields = this.getSearchableFields(); - fields.forEach((key, index) => { - const queryOperator = fields.length - 1 === index ? null : 'OR'; - payload.push({ - attribute_key: key, - filter_operator: 'contains', - values: values, - query_operator: queryOperator, + + fields.forEach((field, index1) => { + values.forEach((number, index2) => { + const queryOperator = fields.length - 1 === index1 && values.length - 1 === index2 ? null : 'OR'; + payload.push({ + attribute_key: field, + filter_operator: 'contains', + values: [number], + query_operator: queryOperator, + }); }); }); + this.logger.verbose('Payload: ' + JSON.stringify(payload)); return payload; } - public async createConversation(instance: InstanceDto, body: any) { this.logger.verbose('create conversation to instance: ' + instance.instanceName); try { From 54603002a68976dfa126c48ff1adc80bef7df090 Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Thu, 1 Feb 2024 22:13:19 -0300 Subject: [PATCH 066/144] Fix for brazil 9 digit --- src/whatsapp/services/chatwoot.service.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 729dcb31..c725e246 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -447,29 +447,28 @@ export class ChatwootService { } private getSearchableFields() { - return ['identifier', 'phone_number', 'name', 'email']; + return ['phone_number']; } private getFilterPayload(query: string) { - const payload = []; - const values = this.getNumbers(query); + const filterPayload = []; - const fields = this.getSearchableFields(); + const numbers = this.getNumbers(query); + const fieldsToSearch = this.getSearchableFields(); - fields.forEach((field, index1) => { - values.forEach((number, index2) => { - const queryOperator = fields.length - 1 === index1 && values.length - 1 === index2 ? null : 'OR'; - payload.push({ + fieldsToSearch.forEach((field, index1) => { + numbers.forEach((number, index2) => { + const queryOperator = fieldsToSearch.length - 1 === index1 && numbers.length - 1 === index2 ? null : 'OR'; + filterPayload.push({ attribute_key: field, - filter_operator: 'contains', - values: [number], + filter_operator: 'equal_to', + values: [number.replace('+', '')], query_operator: queryOperator, }); }); }); - this.logger.verbose('Payload: ' + JSON.stringify(payload)); - return payload; + return filterPayload; } public async createConversation(instance: InstanceDto, body: any) { this.logger.verbose('create conversation to instance: ' + instance.instanceName); From b3adde3a7a1494672a2591aca494878409abb7ae Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Thu, 1 Feb 2024 23:42:52 -0300 Subject: [PATCH 067/144] Feat Reject Message If is not a valid wpp number --- src/utils/translations/en.json | 3 ++- src/utils/translations/pt-BR.json | 3 ++- src/whatsapp/services/chatwoot.service.ts | 21 +++++++++++++++++++++ src/whatsapp/services/whatsapp.service.ts | 8 ++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/utils/translations/en.json b/src/utils/translations/en.json index f92c4e08..d8566c72 100644 --- a/src/utils/translations/en.json +++ b/src/utils/translations/en.json @@ -1,5 +1,6 @@ { "qrgeneratedsuccesfully": "QRCode successfully generated!", "scanqr": "Scan this QR code within the next 40 seconds.", - "qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again." + "qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.", + "numbernotinwhatsapp": "The message was not sent as the contact is not a valid Whatsapp number." } \ No newline at end of file diff --git a/src/utils/translations/pt-BR.json b/src/utils/translations/pt-BR.json index d48ff148..a9668848 100644 --- a/src/utils/translations/pt-BR.json +++ b/src/utils/translations/pt-BR.json @@ -1,5 +1,6 @@ { "qrgeneratedsuccesfully": "QRCode gerado com sucesso!", "scanqr": "Escanei o QRCode com o Whatsapp nos próximos 40 segundos.", - "qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa." + "qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.", + "numbernotinwhatsapp": "A mensagem não foi enviada, pois o contato não é um número válido do Whatsapp." } \ No newline at end of file diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0bc74ecd..1ceeb2dd 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1684,6 +1684,27 @@ export class ChatwootService { return null; } + if (event === 'contact.is_not_in_wpp') { + const getConversation = await this.createConversation(instance, body); + + if (!getConversation) { + this.logger.warn('conversation not found'); + return; + } + + client.messages.create({ + accountId: this.provider.account_id, + conversationId: getConversation, + data: { + content: `🚨 ${i18next.t('numbernotinwhatsapp')}`, + message_type: 'incoming', + private: true, + }, + }); + + return; + } + if (event === 'messages.upsert' || event === 'send.message') { this.logger.verbose('event messages.upsert'); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 71ca431e..5c562b3c 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2357,7 +2357,15 @@ export class WAStartupService { const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + if (this.localChatwoot.enabled) { + const body = { + key: { remoteJid: isWA.jid }, + }; + + this.chatwootService.eventWhatsapp('contact.is_not_in_wpp', { instanceName: this.instance.name }, body); + } throw new BadRequestException(isWA); } From c130846fe83ad2e55bc4d6230927e717e026565a Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Fri, 2 Feb 2024 08:57:44 -0300 Subject: [PATCH 068/144] Change message_type Change message_type to 'outgoing' --- 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 1ceeb2dd..bc7d4471 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1697,7 +1697,7 @@ export class ChatwootService { conversationId: getConversation, data: { content: `🚨 ${i18next.t('numbernotinwhatsapp')}`, - message_type: 'incoming', + message_type: 'outgoing', private: true, }, }); From 8a5ebe83a3420f193224988ef0f9dd074285fe38 Mon Sep 17 00:00:00 2001 From: jaison-x Date: Wed, 31 Jan 2024 11:20:29 -0300 Subject: [PATCH 069/144] feat(chatwoot): import history messages to chatwoot on whatsapp connection Messages are imported direct to chatwoot database. Media and group messages are ignored. New env.yml variables: CHATWOOT_IMPORT_DATABASE_CONNECTION_URI: URI to connect direct on chatwoot database. CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE: Indicates to use a text placeholder on media messages. New instance setting: sync_full_history: Indicates to request a full history sync to baileys. New chatwoot options: import_contacts: Indicates to import contacts. import_messages: Indicates to import messages. days_limit_import_messages: Number of days to limit history messages to import. --- package.json | 3 +- src/config/env.config.ts | 23 +- src/dev-env.yml | 10 +- src/docs/swagger.yaml | 12 + src/libs/postgres.client.ts | 49 ++ src/utils/chatwoot-import-helper.ts | 472 ++++++++++++++++++ src/validate/validate.schema.ts | 8 +- .../controllers/chatwoot.controller.ts | 3 + .../controllers/instance.controller.ts | 11 + src/whatsapp/dto/chatwoot.dto.ts | 3 + src/whatsapp/dto/instance.dto.ts | 4 + src/whatsapp/dto/settings.dto.ts | 1 + src/whatsapp/models/chat.model.ts | 5 + src/whatsapp/models/chatwoot.model.ts | 6 + src/whatsapp/models/contact.model.ts | 5 + src/whatsapp/models/message.model.ts | 7 + src/whatsapp/models/settings.model.ts | 2 + src/whatsapp/repository/chat.repository.ts | 5 +- src/whatsapp/repository/contact.repository.ts | 5 +- src/whatsapp/repository/message.repository.ts | 12 +- src/whatsapp/services/chatwoot.service.ts | 190 +++++-- src/whatsapp/services/whatsapp.service.ts | 263 +++++++--- src/whatsapp/types/wa.types.ts | 4 + 23 files changed, 992 insertions(+), 111 deletions(-) create mode 100644 src/libs/postgres.client.ts create mode 100644 src/utils/chatwoot-import-helper.ts diff --git a/package.json b/package.json index 6f976f43..3c26f4de 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "node-mime-types": "^1.1.0", "node-windows": "^1.0.0-beta.8", "parse-bmfont-xml": "^1.1.4", + "pg": "^8.11.3", "pino": "^8.11.0", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", @@ -112,4 +113,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^4.9.5" } -} +} \ No newline at end of file diff --git a/src/config/env.config.ts b/src/config/env.config.ts index fd6187ab..22ea9b21 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -149,7 +149,18 @@ 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 ChatWoot = { MESSAGE_DELETE: boolean }; +export type Chatwoot = { + MESSAGE_DELETE: boolean; + IMPORT: { + DATABASE: { + CONNECTION: { + URI: string; + }; + }; + PLACEHOLDER_MEDIA_MESSAGE: boolean; + }; +}; + export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type Production = boolean; @@ -171,7 +182,7 @@ export interface Env { CONFIG_SESSION_PHONE: ConfigSessionPhone; QRCODE: QrCode; TYPEBOT: Typebot; - CHATWOOT: ChatWoot; + CHATWOOT: Chatwoot; CACHE: CacheConf; AUTHENTICATION: Auth; PRODUCTION?: Production; @@ -338,6 +349,14 @@ export class ConfigService { }, CHATWOOT: { MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'false', + IMPORT: { + DATABASE: { + CONNECTION: { + URI: process.env.CHATWOOT_DATABASE_CONNECTION_URI || '', + }, + }, + PLACEHOLDER_MEDIA_MESSAGE: process.env?.CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE === 'true', + }, }, CACHE: { REDIS: { diff --git a/src/dev-env.yml b/src/dev-env.yml index 6f09f6ca..07e71453 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -153,10 +153,16 @@ TYPEBOT: API_VERSION: 'old' # old | latest KEEP_OPEN: false -# If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot. CHATWOOT: + # If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot. MESSAGE_DELETE: true # false | true - + IMPORT: + # This db connection is used to import messages from whatsapp to chatwoot database + DATABASE: + CONNECTION: + URI: "postgres://user:password@hostname:port/dbname" + PLACEHOLDER_MEDIA_MESSAGE: true + # Cache to optimize application performance CACHE: REDIS: diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index d3301cae..b5b27c38 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -2076,6 +2076,9 @@ paths: read_status: type: boolean description: "Indicates whether to mark status messages as read." + sync_full_history: + type: boolean + description: "Indicates whether to request a full history messages sync on connect." parameters: - name: instanceName in: path @@ -2141,6 +2144,15 @@ paths: conversation_pending: type: boolean description: "Indicates whether to mark conversations as pending." + import_contacts: + type: boolean + description: "Indicates whether to import contacts from phone to Chatwoot when connecting." + import_messages: + type: boolean + description: "Indicates whether to import messages from phone to Chatwoot when connecting." + days_limit_import_messages: + type: number + description: "Indicates number of days to limit messages imported to Chatwoot." parameters: - name: instanceName in: path diff --git a/src/libs/postgres.client.ts b/src/libs/postgres.client.ts new file mode 100644 index 00000000..d1a68cdf --- /dev/null +++ b/src/libs/postgres.client.ts @@ -0,0 +1,49 @@ +import postgresql from 'pg'; + +import { Chatwoot, configService } from '../config/env.config'; +import { Logger } from '../config/logger.config'; + +const { Pool } = postgresql; + +class Postgres { + private logger = new Logger(Postgres.name); + private pool; + private connected = false; + + getConnection(connectionString: string) { + if (this.connected) { + return this.pool; + } else { + this.pool = new Pool({ + connectionString, + ssl: { + rejectUnauthorized: false, + }, + }); + + this.pool.on('error', () => { + this.logger.error('postgres disconnected'); + this.connected = false; + }); + + try { + this.logger.verbose('connecting new postgres'); + this.connected = true; + } catch (e) { + this.connected = false; + this.logger.error('postgres connect exception caught: ' + e); + return null; + } + + return this.pool; + } + } + + getChatwootConnection() { + const uri = configService.get('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI; + + return this.getConnection(uri); + } +} + +export const postgresClient = new Postgres(); diff --git a/src/utils/chatwoot-import-helper.ts b/src/utils/chatwoot-import-helper.ts new file mode 100644 index 00000000..3283683f --- /dev/null +++ b/src/utils/chatwoot-import-helper.ts @@ -0,0 +1,472 @@ +import { inbox } from '@figuro/chatwoot-sdk'; +import { proto } from '@whiskeysockets/baileys'; + +import { Chatwoot, configService } from '../config/env.config'; +import { Logger } from '../config/logger.config'; +import { postgresClient } from '../libs/postgres.client'; +import { InstanceDto } from '../whatsapp/dto/instance.dto'; +import { ChatwootRaw, ContactRaw, MessageRaw } from '../whatsapp/models'; +import { ChatwootService } from '../whatsapp/services/chatwoot.service'; + +type ChatwootUser = { + user_type: string; + user_id: number; +}; + +type FksChatwoot = { + phone_number: string; + contact_id: string; + conversation_id: string; +}; + +type firstLastTimestamp = { + first: number; + last: number; +}; + +type IWebMessageInfo = Omit & Partial>; + +class ChatwootImport { + private logger = new Logger(ChatwootImport.name); + private repositoryMessagesCache = new Map>(); + private historyMessages = new Map(); + private historyContacts = new Map(); + + public getRepositoryMessagesCache(instance: InstanceDto) { + return this.repositoryMessagesCache.has(instance.instanceName) + ? this.repositoryMessagesCache.get(instance.instanceName) + : null; + } + + public setRepositoryMessagesCache(instance: InstanceDto, repositoryMessagesCache: Set) { + this.repositoryMessagesCache.set(instance.instanceName, repositoryMessagesCache); + } + + public deleteRepositoryMessagesCache(instance: InstanceDto) { + this.repositoryMessagesCache.delete(instance.instanceName); + } + + public addHistoryMessages(instance: InstanceDto, messagesRaw: MessageRaw[]) { + const actualValue = this.historyMessages.has(instance.instanceName) + ? this.historyMessages.get(instance.instanceName) + : []; + this.historyMessages.set(instance.instanceName, actualValue.concat(messagesRaw)); + } + + public addHistoryContacts(instance: InstanceDto, contactsRaw: ContactRaw[]) { + const actualValue = this.historyContacts.has(instance.instanceName) + ? this.historyContacts.get(instance.instanceName) + : []; + this.historyContacts.set(instance.instanceName, actualValue.concat(contactsRaw)); + } + + public deleteHistoryMessages(instance: InstanceDto) { + this.historyMessages.delete(instance.instanceName); + } + + public deleteHistoryContacts(instance: InstanceDto) { + this.historyContacts.delete(instance.instanceName); + } + + public clearAll(instance: InstanceDto) { + this.deleteRepositoryMessagesCache(instance); + this.deleteHistoryMessages(instance); + this.deleteHistoryContacts(instance); + } + + public getHistoryMessagesLenght(instance: InstanceDto) { + return this.historyMessages.get(instance.instanceName)?.length ?? 0; + } + + public async importHistoryContacts(instance: InstanceDto, provider: ChatwootRaw) { + try { + if (this.getHistoryMessagesLenght(instance) > 0) { + return; + } + + const pgClient = postgresClient.getChatwootConnection(); + + let totalContactsImported = 0; + + const contacts = this.historyContacts.get(instance.instanceName) || []; + if (contacts.length === 0) { + return 0; + } + + let contactsChunk: ContactRaw[] = this.sliceIntoChunks(contacts, 3000); + while (contactsChunk.length > 0) { + // inserting contacts in chatwoot db + let sqlInsert = `INSERT INTO contacts + (name, phone_number, account_id, identifier, created_at, updated_at) VALUES `; + const bindInsert = [provider.account_id]; + + for (const contact of contactsChunk) { + bindInsert.push(contact.pushName); + const bindName = `$${bindInsert.length}`; + + bindInsert.push(`+${contact.id.split('@')[0]}`); + const bindPhoneNumber = `$${bindInsert.length}`; + + bindInsert.push(contact.id); + const bindIdentifier = `$${bindInsert.length}`; + + sqlInsert += `(${bindName}, ${bindPhoneNumber}, $1, ${bindIdentifier}, NOW(), NOW()),`; + } + if (sqlInsert.slice(-1) === ',') { + sqlInsert = sqlInsert.slice(0, -1); + } + sqlInsert += ` ON CONFLICT (identifier, account_id) + DO UPDATE SET + name = EXCLUDED.name, + phone_number = EXCLUDED.phone_number, + identifier = EXCLUDED.identifier`; + + totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0; + contactsChunk = this.sliceIntoChunks(contacts, 3000); + } + + this.deleteHistoryContacts(instance); + + return totalContactsImported; + } catch (error) { + this.logger.error(`Error on import history contacts: ${error.toString()}`); + } + } + + public async importHistoryMessages( + instance: InstanceDto, + chatwootService: ChatwootService, + inbox: inbox, + provider: ChatwootRaw, + ) { + try { + const pgClient = postgresClient.getChatwootConnection(); + + const chatwootUser = await this.getChatwootUser(provider); + if (!chatwootUser) { + throw new Error('User not found to import messages.'); + } + + let totalMessagesImported = 0; + + const messagesOrdered = this.historyMessages.get(instance.instanceName) || []; + if (messagesOrdered.length === 0) { + return 0; + } + + // ordering messages by number and timestamp asc + messagesOrdered.sort((a, b) => { + return ( + parseInt(a.key.remoteJid) - parseInt(b.key.remoteJid) || + (a.messageTimestamp as number) - (b.messageTimestamp as number) + ); + }); + + const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered); + // Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number} + const phoneNumbersWithTimestamp = new Map(); + allMessagesMappedByPhoneNumber.forEach((messages: MessageRaw[], phoneNumber: string) => { + phoneNumbersWithTimestamp.set(phoneNumber, { + first: messages[0]?.messageTimestamp as number, + last: messages[messages.length - 1]?.messageTimestamp as number, + }); + }); + + // processing messages in batch + const batchSize = 4000; + let messagesChunk: MessageRaw[] = this.sliceIntoChunks(messagesOrdered, batchSize); + while (messagesChunk.length > 0) { + // Map structure: +552199999999 => MessageRaw[] + const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk); + + if (messagesByPhoneNumber.size > 0) { + const fksByNumber = await this.selectOrCreateFksFromChatwoot( + provider, + inbox, + phoneNumbersWithTimestamp, + messagesByPhoneNumber, + ); + + // inserting messages in chatwoot db + let sqlInsertMsg = `INSERT INTO messages + (content, account_id, inbox_id, conversation_id, message_type, private, content_type, + sender_type, sender_id, created_at, updated_at) VALUES `; + const bindInsertMsg = [provider.account_id, inbox.id]; + + messagesByPhoneNumber.forEach((messages: MessageRaw[], phoneNumber: string) => { + const fksChatwoot = fksByNumber.get(phoneNumber); + + messages.forEach((message) => { + if (!message.message) { + return; + } + + if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) { + return; + } + + const contentMessage = this.getContentMessage(chatwootService, message); + if (!contentMessage) { + return; + } + + bindInsertMsg.push(contentMessage); + const bindContent = `$${bindInsertMsg.length}`; + + bindInsertMsg.push(fksChatwoot.conversation_id); + const bindConversationId = `$${bindInsertMsg.length}`; + + bindInsertMsg.push(message.key.fromMe ? '1' : '0'); + const bindMessageType = `$${bindInsertMsg.length}`; + + bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_type : 'Contact'); + const bindSenderType = `$${bindInsertMsg.length}`; + + bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id); + const bindSenderId = `$${bindInsertMsg.length}`; + + bindInsertMsg.push(message.messageTimestamp as number); + const bindmessageTimestamp = `$${bindInsertMsg.length}`; + + sqlInsertMsg += `(${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0, + ${bindSenderType},${bindSenderId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`; + }); + }); + if (bindInsertMsg.length > 2) { + if (sqlInsertMsg.slice(-1) === ',') { + sqlInsertMsg = sqlInsertMsg.slice(0, -1); + } + totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0; + } + } + messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize); + } + + this.deleteHistoryMessages(instance); + this.deleteRepositoryMessagesCache(instance); + + this.importHistoryContacts(instance, provider); + + return totalMessagesImported; + } catch (error) { + this.logger.error(`Error on import history messages: ${error.toString()}`); + + this.deleteHistoryMessages(instance); + this.deleteRepositoryMessagesCache(instance); + } + } + + public async selectOrCreateFksFromChatwoot( + provider: ChatwootRaw, + inbox: inbox, + phoneNumbersWithTimestamp: Map, + messagesByPhoneNumber: Map, + ): Promise> { + const pgClient = postgresClient.getChatwootConnection(); + + const bindValues = [provider.account_id, inbox.id]; + const phoneNumberBind = Array.from(messagesByPhoneNumber.keys()) + .map((phoneNumber) => { + const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber); + + if (phoneNumberTimestamp) { + bindValues.push(phoneNumber); + let bindStr = `($${bindValues.length},`; + + bindValues.push(phoneNumberTimestamp.first); + bindStr += `$${bindValues.length},`; + + bindValues.push(phoneNumberTimestamp.last); + return `${bindStr}$${bindValues.length})`; + } + }) + .join(','); + + // select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db + const sqlFromChatwoot = `WITH + phone_number AS ( + SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM ( + VALUES + ${phoneNumberBind} + ) as t (phone_number, created_at, last_activity_at) + ), + + only_new_phone_number AS ( + SELECT * FROM phone_number + WHERE phone_number NOT IN ( + SELECT phone_number + FROM contacts + JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2 + JOIN conversations con ON con.contact_inbox_id = ci.id + AND con.account_id = $1 + AND con.inbox_id = $2 + AND con.contact_id = contacts.id + WHERE contacts.account_id = $1 + ) + ), + + new_contact AS ( + INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at) + SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''), + '@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at) + FROM only_new_phone_number AS p + ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at + RETURNING id, phone_number, created_at, updated_at + ), + + new_contact_inbox AS ( + INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at) + SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at + FROM new_contact + RETURNING id, contact_id, created_at, updated_at + ), + + new_conversation AS ( + INSERT INTO conversations (account_id, inbox_id, status, contact_id, + contact_inbox_id, uuid, last_activity_at, created_at, updated_at) + SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(), + new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at + FROM new_contact_inbox + RETURNING id, contact_id + ) + + SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id + FROM new_conversation + JOIN new_contact ON new_conversation.contact_id = new_contact.id + + UNION + + SELECT p.phone_number, c.id contact_id, con.id conversation_id + FROM phone_number p + JOIN contacts c ON c.phone_number = p.phone_number + JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2 + JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1 + AND con.inbox_id = $2 AND con.contact_id = c.id`; + + const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues); + + return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item])); + } + + public async getChatwootUser(provider: ChatwootRaw): Promise { + try { + const pgClient = postgresClient.getChatwootConnection(); + + const sqlUser = `SELECT owner_type AS user_type, owner_id AS user_id + FROM access_tokens + WHERE token = $1`; + + return (await pgClient.query(sqlUser, [provider.token]))?.rows[0] || false; + } catch (error) { + this.logger.error(`Error on getChatwootUser: ${error.toString()}`); + } + } + + public createMessagesMapByPhoneNumber(messages: MessageRaw[]): Map { + return messages.reduce((acc: Map, message: MessageRaw) => { + if (!this.isIgnorePhoneNumber(message?.key?.remoteJid)) { + const phoneNumber = message?.key?.remoteJid?.split('@')[0]; + if (phoneNumber) { + const phoneNumberPlus = `+${phoneNumber}`; + const messages = acc.has(phoneNumberPlus) ? acc.get(phoneNumberPlus) : []; + messages.push(message); + acc.set(phoneNumberPlus, messages); + } + } + + return acc; + }, new Map()); + } + + public async getContactsOrderByRecentConversations( + inbox: inbox, + provider: ChatwootRaw, + limit = 50, + ): Promise<{ id: number; phone_number: string; identifier: string }[]> { + try { + const pgClient = postgresClient.getChatwootConnection(); + + const sql = `SELECT contacts.id, contacts.identifier, contacts.phone_number + FROM conversations + JOIN contacts ON contacts.id = conversations.contact_id + WHERE conversations.account_id = $1 + AND inbox_id = $2 + ORDER BY conversations.last_activity_at DESC + LIMIT $3`; + + return (await pgClient.query(sql, [provider.account_id, inbox.id, limit]))?.rows; + } catch (error) { + this.logger.error(`Error on get recent conversations: ${error.toString()}`); + } + } + + public getContentMessage(chatwootService: ChatwootService, msg: IWebMessageInfo) { + const contentMessage = chatwootService.getConversationMessage(msg.message); + if (contentMessage) { + return contentMessage; + } + + if (!configService.get('CHATWOOT').IMPORT.PLACEHOLDER_MEDIA_MESSAGE) { + return ''; + } + + const types = { + documentMessage: msg.message.documentMessage, + documentWithCaptionMessage: msg.message.documentWithCaptionMessage?.message?.documentMessage, + imageMessage: msg.message.imageMessage, + videoMessage: msg.message.videoMessage, + audioMessage: msg.message.audioMessage, + stickerMessage: msg.message.stickerMessage, + templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText, + }; + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + switch (typeKey) { + case 'documentMessage': + return `__`; + + case 'documentWithCaptionMessage': + return `__`; + + case 'templateMessage': + return msg.message.templateMessage.hydratedTemplate.hydratedTitleText + ? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n` + : '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText; + + case 'imageMessage': + return '__'; + + case 'videoMessage': + return '_