From 7c3fccb8efef01aa62c168ab3757603c1e7305c7 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 9 Apr 2025 15:48:11 -0300 Subject: [PATCH 01/30] feat: sendsticket notconvertsticket --- src/api/dto/sendMessage.dto.ts | 1 + .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index 1c9b1154..ba9ecf52 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -44,6 +44,7 @@ export class Metadata { mentionsEveryOne?: boolean; mentioned?: string[]; encoding?: boolean; + notConvertSticker?: boolean; } export class SendTextDto extends Metadata { diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 42695d8d..fa453ee0 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2342,7 +2342,9 @@ export class BaileysStartupService extends ChannelStartupService { if (file) mediaData.sticker = file.buffer.toString('base64'); - const convert = await this.convertToWebP(data.sticker); + const convert = data?.notConvertSticker + ? Buffer.from(data.sticker, 'base64') + : await this.convertToWebP(data.sticker); const gifPlayback = data.sticker.includes('.gif'); const result = await this.sendMessageWithTyping( data.number, From 9ca4bfadabe71aad1ea24168ff7d9d3b1846f343 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Thu, 10 Apr 2025 15:44:29 -0300 Subject: [PATCH 02/30] =?UTF-8?q?feat:=20Criado=20um=20novo=20grupo=20de?= =?UTF-8?q?=20rotas=20(business)=20para=20tratar=20dos=20catalogos=20de=20?= =?UTF-8?q?produtos=20e=20Cole=C3=A7=C3=B5es=20evitando=20altera=C3=A7?= =?UTF-8?q?=C3=B5es=20desnecess=C3=A1rias=20em=20arquivos=20do=20reposit?= =?UTF-8?q?=C3=B3rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/business.controller.ts | 15 ++++++ src/api/controllers/chat.controller.ts | 10 ---- src/api/dto/business.dto.ts | 14 +++++ src/api/dto/chat.dto.ts | 11 ---- .../whatsapp/whatsapp.baileys.service.ts | 52 +++++++++++++------ src/api/routes/business.router.ts | 37 +++++++++++++ src/api/routes/chat.router.ts | 24 --------- src/api/routes/index.router.ts | 2 + src/api/server.module.ts | 2 + src/validate/business.schema.ts | 17 ++++++ src/validate/chat.schema.ts | 18 ------- src/validate/validate.schema.ts | 1 + 12 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 src/api/controllers/business.controller.ts create mode 100644 src/api/dto/business.dto.ts create mode 100644 src/api/routes/business.router.ts create mode 100644 src/validate/business.schema.ts diff --git a/src/api/controllers/business.controller.ts b/src/api/controllers/business.controller.ts new file mode 100644 index 00000000..3c7f166c --- /dev/null +++ b/src/api/controllers/business.controller.ts @@ -0,0 +1,15 @@ +import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; +import { InstanceDto } from '@api/dto/instance.dto'; +import { WAMonitoringService } from '@api/services/monitor.service'; + +export class BusinessController { + constructor(private readonly waMonitor: WAMonitoringService) {} + + public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) { + return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data); + } + + public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) { + return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data); + } +} diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 8c922f11..207d8ba5 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -3,8 +3,6 @@ import { BlockUserDto, DeleteMessage, getBase64FromMediaMessageDto, - getCatalogDto, - getCollectionsDto, MarkChatUnreadDto, NumberDto, PrivacySettingDto, @@ -111,12 +109,4 @@ export class ChatController { public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) { return await this.waMonitor.waInstances[instanceName].blockUser(data); } - - public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) { - return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data); - } - - public async fetchCatalogCollections({ instanceName }: InstanceDto, data: getCollectionsDto) { - return await this.waMonitor.waInstances[instanceName].fetchCatalogCollections(instanceName, data); - } } diff --git a/src/api/dto/business.dto.ts b/src/api/dto/business.dto.ts new file mode 100644 index 00000000..d29b3cf9 --- /dev/null +++ b/src/api/dto/business.dto.ts @@ -0,0 +1,14 @@ +export class NumberDto { + number: string; +} + +export class getCatalogDto { + number?: string; + limit?: number; + cursor?: string; +} + +export class getCollectionsDto { + number?: string; + limit?: number; +} diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 1693165e..00da7fdd 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -126,14 +126,3 @@ export class BlockUserDto { number: string; status: 'block' | 'unblock'; } - -export class getCatalogDto { - number?: string; - limit?: number; - cursor?: string; -} - -export class getCollectionsDto { - number?: string; - limit?: number; -} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index fa453ee0..d112041c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1,11 +1,10 @@ +import { getCollectionsDto } from '@api/dto/business.dto'; import { OfferCallDto } from '@api/dto/call.dto'; import { ArchiveChatDto, BlockUserDto, DeleteMessage, getBase64FromMediaMessageDto, - getCatalogDto, - getCollectionsDto, LastMessage, MarkChatUnreadDto, NumberBusiness, @@ -2343,8 +2342,8 @@ export class BaileysStartupService extends ChannelStartupService { if (file) mediaData.sticker = file.buffer.toString('base64'); const convert = data?.notConvertSticker - ? Buffer.from(data.sticker, 'base64') - : await this.convertToWebP(data.sticker); + ? Buffer.from(data.sticker, 'base64') + : await this.convertToWebP(data.sticker); const gifPlayback = data.sticker.includes('.gif'); const result = await this.sendMessageWithTyping( data.number, @@ -4024,11 +4023,11 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - //Catalogs and collections - public async fetchCatalog(instanceName: string, data: getCatalogDto) { + //Business Controller + public async fetchCatalog(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; const limit = data.limit || 10; - const cursor = data.cursor || null; + const cursor = null; const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); @@ -4039,15 +4038,35 @@ export class BaileysStartupService extends ChannelStartupService { try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); const business = await this.fetchBusinessProfile(info?.jid); - const catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); + + let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); + let nextPageCursor = catalog.nextPageCursor; + let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; + let pagination = nextPageCursorJson?.pagination_cursor + ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) + : null; + let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; + + let productsCatalog = catalog.products || []; + let countLoops = 0; + while (fetcherHasMore && countLoops < 4) { + catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor }); + nextPageCursor = catalog.nextPageCursor; + nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null; + pagination = nextPageCursorJson?.pagination_cursor + ? JSON.parse(atob(nextPageCursorJson.pagination_cursor)) + : null; + fetcherHasMore = pagination?.fetcher_has_more === true ? true : false; + productsCatalog = [...productsCatalog, ...catalog.products]; + countLoops++; + } return { wuid: info?.jid || jid, - name: info?.name, numberExists: info?.exists, isBusiness: business.isBusiness, - catalogLength: catalog?.products.length, - catalog: catalog?.products, + catalogLength: productsCatalog.length, + catalog: productsCatalog, }; } catch (error) { console.log(error); @@ -4082,9 +4101,9 @@ export class BaileysStartupService extends ChannelStartupService { } } - public async fetchCatalogCollections(instanceName: string, data: getCollectionsDto) { + public async fetchCollections(instanceName: string, data: getCollectionsDto) { const jid = data.number ? createJid(data.number) : this.client?.user?.id; - const limit = data.limit || 10; + const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque) const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); @@ -4095,18 +4114,17 @@ export class BaileysStartupService extends ChannelStartupService { try { const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); const business = await this.fetchBusinessProfile(info?.jid); - const catalogCollections = await this.getCollections(info?.jid, limit); + const collections = await this.getCollections(info?.jid, limit); return { wuid: info?.jid || jid, name: info?.name, numberExists: info?.exists, isBusiness: business.isBusiness, - catalogLength: catalogCollections?.length, - catalogCollections: catalogCollections, + collectionsLength: collections?.length, + collections: collections, }; } catch (error) { - console.log(error); return { wuid: jid, name: null, diff --git a/src/api/routes/business.router.ts b/src/api/routes/business.router.ts new file mode 100644 index 00000000..1e510a4f --- /dev/null +++ b/src/api/routes/business.router.ts @@ -0,0 +1,37 @@ +import { RouterBroker } from '@api/abstract/abstract.router'; +import { NumberDto } from '@api/dto/chat.dto'; +import { businessController } from '@api/server.module'; +import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; +import { RequestHandler, Router } from 'express'; + +import { HttpStatus } from './index.router'; + +export class BusinessRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('getCatalog'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: catalogSchema, + ClassRef: NumberDto, + execute: (instance, data) => businessController.fetchCatalog(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) + + .post(this.routerPath('getCollections'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: collectionsSchema, + ClassRef: NumberDto, + execute: (instance, data) => businessController.fetchCollections(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router: Router = Router(); +} diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index aac9fe39..20126c1a 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -22,8 +22,6 @@ import { Contact, Message, MessageUpdate } from '@prisma/client'; import { archiveChatSchema, blockUserSchema, - catalogSchema, - collectionsSchema, contactValidateSchema, deleteMessageSchema, markChatUnreadSchema, @@ -269,28 +267,6 @@ export class ChatRouter extends RouterBroker { }); return res.status(HttpStatus.CREATED).json(response); - }) - - .post(this.routerPath('fetchCatalog'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: catalogSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchCatalog(instance, data), - }); - - return res.status(HttpStatus.OK).json(response); - }) - - .post(this.routerPath('fetchCollections'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: collectionsSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchCatalogCollections(instance, data), - }); - - return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index f671f1f6..688f2109 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -8,6 +8,7 @@ import { configService } from '@config/env.config'; import { Router } from 'express'; import fs from 'fs'; +import { BusinessRouter } from './business.router'; import { CallRouter } from './call.router'; import { ChatRouter } from './chat.router'; import { GroupRouter } from './group.router'; @@ -61,6 +62,7 @@ router .use('/instance', new InstanceRouter(configService, ...guards).router) .use('/message', new MessageRouter(...guards).router) .use('/call', new CallRouter(...guards).router) + .use('/business', new BusinessRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) .use('/template', new TemplateRouter(configService, ...guards).router) diff --git a/src/api/server.module.ts b/src/api/server.module.ts index ec32dede..a9cfc9c0 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -3,6 +3,7 @@ import { configService, ProviderSession } from '@config/env.config'; import { eventEmitter } from '@config/event.config'; import { Logger } from '@config/logger.config'; +import { BusinessController } from './controllers/business.controller'; import { CallController } from './controllers/call.controller'; import { ChatController } from './controllers/chat.controller'; import { GroupController } from './controllers/group.controller'; @@ -74,6 +75,7 @@ export const instanceController = new InstanceController( export const sendMessageController = new SendMessageController(waMonitor); export const callController = new CallController(waMonitor); export const chatController = new ChatController(waMonitor); +export const businessController = new BusinessController(waMonitor); export const groupController = new GroupController(waMonitor); export const labelController = new LabelController(waMonitor); diff --git a/src/validate/business.schema.ts b/src/validate/business.schema.ts new file mode 100644 index 00000000..91ad17b2 --- /dev/null +++ b/src/validate/business.schema.ts @@ -0,0 +1,17 @@ +import { JSONSchema7 } from 'json-schema'; + +export const catalogSchema: JSONSchema7 = { + type: 'object', + properties: { + number: { type: 'string' }, + limit: { type: 'number' }, + }, +}; + +export const collectionsSchema: JSONSchema7 = { + type: 'object', + properties: { + number: { type: 'string' }, + limit: { type: 'number' }, + }, +}; diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index fd324c10..dba27995 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -315,21 +315,3 @@ export const profileSchema: JSONSchema7 = { isBusiness: { type: 'boolean' }, }, }; - -export const catalogSchema: JSONSchema7 = { - type: 'object', - properties: { - number: { type: 'string' }, - limit: { type: 'number' }, - cursor: { type: 'string' }, - }, -}; - -export const collectionsSchema: JSONSchema7 = { - type: 'object', - properties: { - number: { type: 'string' }, - limit: { type: 'number' }, - cursor: { type: 'string' }, - }, -}; diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 9486960b..c0ab1c6d 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -1,4 +1,5 @@ // Integrations Schema +export * from './business.schema'; export * from './chat.schema'; export * from './group.schema'; export * from './instance.schema'; From 244e3f8e5147d5f063e2450150cef15146ebc516 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Fri, 11 Apr 2025 10:41:21 -0300 Subject: [PATCH 03/30] Fix case in table name --- prisma/mysql-schema.prisma | 2 +- prisma/postgresql-schema.prisma | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index a73ca069..d93734e4 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -99,7 +99,7 @@ model Instance { Template Template[] Dify Dify[] DifySetting DifySetting? - integrationSessions IntegrationSession[] + IntegrationSessions IntegrationSession[] EvolutionBot EvolutionBot[] EvolutionBotSetting EvolutionBotSetting? Flowise Flowise[] diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index a9782ce5..3c94b808 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -99,7 +99,7 @@ model Instance { Template Template[] Dify Dify[] DifySetting DifySetting? - integrationSessions IntegrationSession[] + IntegrationSessions IntegrationSession[] EvolutionBot EvolutionBot[] EvolutionBotSetting EvolutionBotSetting? Flowise Flowise[] From a54b5a5b8ce8162ead46f82e4b250104bf72a58f Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Fri, 11 Apr 2025 14:09:19 -0300 Subject: [PATCH 04/30] Fix table name --- prisma/mysql-schema.prisma | 2 +- prisma/postgresql-schema.prisma | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index d93734e4..3796a9ca 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -99,7 +99,7 @@ model Instance { Template Template[] Dify Dify[] DifySetting DifySetting? - IntegrationSessions IntegrationSession[] + IntegrationSession IntegrationSession[] EvolutionBot EvolutionBot[] EvolutionBotSetting EvolutionBotSetting? Flowise Flowise[] diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 3c94b808..afd5003a 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -99,7 +99,7 @@ model Instance { Template Template[] Dify Dify[] DifySetting DifySetting? - IntegrationSessions IntegrationSession[] + IntegrationSession IntegrationSession[] EvolutionBot EvolutionBot[] EvolutionBotSetting EvolutionBotSetting? Flowise Flowise[] From 6104249ba8b2ba58f70aa6221b46b52fb04c38f0 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Fri, 11 Apr 2025 15:16:40 -0300 Subject: [PATCH 05/30] feat: add message location support meta --- .../channel/meta/whatsapp.business.service.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 001ffbf3..4ac7b8ac 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -200,6 +200,20 @@ export class BusinessStartupService extends ChannelStartupService { return content; } + private messageLocationJson(received: any) { + const message = received.messages[0]; + let content: any = { + locationMessage: { + degreesLatitude: message.location.latitude, + degreesLongitude: message.location.longitude, + name: message.location?.name, + address: message.location?.address, + }, + }; + message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; + return content; + } + private messageContactsJson(received: any) { const message = received.messages[0]; let content: any = {}; @@ -277,6 +291,9 @@ export class BusinessStartupService extends ChannelStartupService { case 'template': messageType = 'conversation'; break; + case 'location': + messageType = 'locationMessage'; + break; default: messageType = 'conversation'; break; @@ -432,6 +449,17 @@ export class BusinessStartupService extends ChannelStartupService { source: 'unknown', instanceId: this.instanceId, }; + } else if (received?.messages[0].location) { + messageRaw = { + key, + pushName, + message: this.messageLocationJson(received), + contextInfo: this.messageLocationJson(received)?.contextInfo, + messageType: this.renderMessageType(received.messages[0].type), + messageTimestamp: parseInt(received.messages[0].timestamp) as number, + source: 'unknown', + instanceId: this.instanceId, + }; } else { messageRaw = { key, From b8740ea2074a157558256601ebcf7ae588045a91 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 14 Apr 2025 14:02:31 -0300 Subject: [PATCH 06/30] =?UTF-8?q?=F0=9F=90=9B=20Corrige=20problema=20na=20?= =?UTF-8?q?API=20relacionado=20=C3=A0=20migration.=20Fixes=20#1234=20Corri?= =?UTF-8?q?ge=20problema=20na=20API=20relacionado=20=C3=A0=20migration.=20?= =?UTF-8?q?Fixes=20#1234?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 prisma/mysql-migrations/1707735894523_add_wavoip_token_to_settings_table/migration.sql diff --git a/prisma/mysql-migrations/1707735894523_add_wavoip_token_to_settings_table/migration.sql b/prisma/mysql-migrations/1707735894523_add_wavoip_token_to_settings_table/migration.sql new file mode 100644 index 00000000..62165eed --- /dev/null +++ b/prisma/mysql-migrations/1707735894523_add_wavoip_token_to_settings_table/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail. + */ + + -- AlterTable + ALTER TABLE `Setting` + ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100); \ No newline at end of file From 6a303778a96faece3a9d43fdc5ecf1c5e334cc24 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 14 Apr 2025 14:30:43 -0300 Subject: [PATCH 07/30] Enhance message editing validation in BaileysStartupService --- .../whatsapp/whatsapp.baileys.service.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index d112041c..7a2aa6df 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3341,6 +3341,16 @@ export class BaileysStartupService extends ChannelStartupService { } try { + const oldMessage: any = await this.getMessage(data.key, true); + if (!oldMessage) throw new NotFoundException('Message not found'); + if (oldMessage?.key?.remoteJid !== jid) { + throw new BadRequestException('RemoteJid does not match'); + } + if (oldMessage?.messageTimestamp > Date.now() + 900000) { + // 15 minutes in milliseconds + throw new BadRequestException('Message is older than 15 minutes'); + } + const response = await this.client.sendMessage(jid, { ...(options as any), edit: data.key, @@ -3365,14 +3375,17 @@ export class BaileysStartupService extends ChannelStartupService { new BadRequestException('You cannot edit deleted messages'); } - const updateMessage = this.prepareMessage({ ...response }); + if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') { + oldMessage.message.conversation = data.text; + } else { + oldMessage.message[oldMessage.messageType].caption = data.text; + } message = await this.prismaRepository.message.update({ where: { id: message.id }, data: { - message: { - ...updateMessage?.message?.[updateMessage.messageType]?.editedMessage, - }, + message: oldMessage.message, status: 'EDITED', + messageTimestamp: Math.floor(Date.now() / 1000), }, }); const messageUpdate: any = { From 8272da8f3cbaf534fbf6b1995919c10a9093a110 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 14 Apr 2025 14:35:09 -0300 Subject: [PATCH 08/30] Implement message update handling in BaileysStartupService --- .../whatsapp/whatsapp.baileys.service.ts | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 7a2aa6df..ddbb9944 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -920,6 +920,27 @@ export class BaileysStartupService extends ChannelStartupService { if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) { if (editedMessage) { await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); + const oldMessage = await this.getMessage(editedMessage.key, true); + if ((oldMessage as any)?.id) { + await this.prismaRepository.message.update({ + where: { id: (oldMessage as any).id }, + data: { + message: editedMessage.editedMessage as any, + messageTimestamp: (editedMessage.timestampMs as Long.Long).toNumber(), + status: 'EDITED', + }, + }); + await this.prismaRepository.messageUpdate.create({ + data: { + fromMe: editedMessage.key.fromMe, + keyId: editedMessage.key.id, + remoteJid: editedMessage.key.remoteJid, + status: 'EDITED', + instanceId: this.instanceId, + messageId: (oldMessage as any).id, + }, + }); + } } } @@ -3342,15 +3363,15 @@ export class BaileysStartupService extends ChannelStartupService { try { const oldMessage: any = await this.getMessage(data.key, true); - if (!oldMessage) throw new NotFoundException('Message not found'); - if (oldMessage?.key?.remoteJid !== jid) { - throw new BadRequestException('RemoteJid does not match'); - } - if (oldMessage?.messageTimestamp > Date.now() + 900000) { - // 15 minutes in milliseconds - throw new BadRequestException('Message is older than 15 minutes'); - } - + if (!oldMessage) throw new NotFoundException('Message not found'); + if (oldMessage?.key?.remoteJid !== jid) { + throw new BadRequestException('RemoteJid does not match'); + } + if (oldMessage?.messageTimestamp > Date.now() + 900000) { + // 15 minutes in milliseconds + throw new BadRequestException('Message is older than 15 minutes'); + } + const response = await this.client.sendMessage(jid, { ...(options as any), edit: data.key, From e7ccbc8bbd129abf513a47afe448a567f5e4b74d Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 14 Apr 2025 14:43:58 -0300 Subject: [PATCH 09/30] fix: change mediaId optional chaining and list response message text obtain --- src/utils/getConversationMessage.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index b2522ab0..ba6c1914 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -4,7 +4,7 @@ const getTypeMessage = (msg: any) => { let mediaId: string; if (configService.get('S3').ENABLE) mediaId = msg.message.mediaUrl; - else mediaId = msg.key.id; + else mediaId = msg.key?.id; const types = { conversation: msg?.message?.conversation, @@ -15,7 +15,8 @@ const getTypeMessage = (msg: any) => { msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, - listResponseMessage: msg?.message?.listResponseMessage?.title, + listResponseMessage: msg?.message?.listResponseMessage?.title || + msg?.listResponseMessage?.title, responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId, templateButtonReplyMessage: msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId, From 42a455f86412562e43550a27d358a21775838b1a Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 14 Apr 2025 14:46:52 -0300 Subject: [PATCH 10/30] fix: obtain mediaUrl not defined --- src/utils/getConversationMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index ba6c1914..90d9f555 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -3,7 +3,7 @@ import { configService, S3 } from '@config/env.config'; const getTypeMessage = (msg: any) => { let mediaId: string; - if (configService.get('S3').ENABLE) mediaId = msg.message.mediaUrl; + if (configService.get('S3').ENABLE) mediaId = msg.message?.mediaUrl; else mediaId = msg.key?.id; const types = { From d206053db6fc25e6775f589f356c7717d80c3084 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Fri, 25 Apr 2025 11:43:02 -0300 Subject: [PATCH 11/30] style: run lint --- src/utils/getConversationMessage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index 90d9f555..723a50da 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -15,8 +15,7 @@ const getTypeMessage = (msg: any) => { msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, - listResponseMessage: msg?.message?.listResponseMessage?.title || - msg?.listResponseMessage?.title, + listResponseMessage: msg?.message?.listResponseMessage?.title || msg?.listResponseMessage?.title, responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId, templateButtonReplyMessage: msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId, From 6c1e8def42a46932006f17caea62228aaf4550ab Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Fri, 25 Apr 2025 13:59:00 -0300 Subject: [PATCH 12/30] hotfix(migration): add missing wavoipToken column in MySQL schema --- .../migration.sql | 175 ++++++++++++++++++ prisma/mysql-migrations/migration_lock.toml | 2 +- 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 prisma/mysql-migrations/20250214181954_add_wavoip_token_column/migration.sql diff --git a/prisma/mysql-migrations/20250214181954_add_wavoip_token_column/migration.sql b/prisma/mysql-migrations/20250214181954_add_wavoip_token_column/migration.sql new file mode 100644 index 00000000..799114c9 --- /dev/null +++ b/prisma/mysql-migrations/20250214181954_add_wavoip_token_column/migration.sql @@ -0,0 +1,175 @@ +/* + Warnings: + + - You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`. + - A unique constraint covering the columns `[instanceId,remoteJid]` on the table `Chat` will be added. If there are existing duplicate values, this will fail. + + */ + -- AlterTable + ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NULL; + + -- AlterTable + ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NULL; + + -- AlterTable + ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL, + MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NULL; + + -- AlterTable + ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP; + + -- AlterTable + ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; + + -- AlterTable + ALTER TABLE `Setting` ADD COLUMN `wavoipToken` VARCHAR(100) NULL, + MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NULL; + + -- AlterTable + ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- AlterTable + ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + MODIFY `updatedAt` TIMESTAMP NOT NULL; + + -- CreateIndex + CREATE UNIQUE INDEX `Chat_instanceId_remoteJid_key` ON `Chat`(`instanceId`, `remoteJid`); \ No newline at end of file diff --git a/prisma/mysql-migrations/migration_lock.toml b/prisma/mysql-migrations/migration_lock.toml index e5a788a7..8a21669a 100644 --- a/prisma/mysql-migrations/migration_lock.toml +++ b/prisma/mysql-migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "mysql" \ No newline at end of file From 64f70b48c282930bbae4e34a9862528b2b56451e Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 14:51:02 -0300 Subject: [PATCH 13/30] fix: Refactor SQS controller to correct bug in sqs events by instance - Implement dynamic queue creation based on enabled events - Add method to list existing queues for an instance - Improve error handling and logging for SQS operations - Remove unused queue removal methods - Update set method to handle queue creation/deletion on event changes - Add comments for future feature of forced queue deletion --- .../integrations/event/sqs/sqs.controller.ts | 170 ++++++++++++------ 1 file changed, 112 insertions(+), 58 deletions(-) diff --git a/src/api/integrations/event/sqs/sqs.controller.ts b/src/api/integrations/event/sqs/sqs.controller.ts index 1d60fb7b..9cb4ef54 100644 --- a/src/api/integrations/event/sqs/sqs.controller.ts +++ b/src/api/integrations/event/sqs/sqs.controller.ts @@ -1,10 +1,11 @@ import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { SQS } from '@aws-sdk/client-sqs'; +import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs'; import { configService, Log, Sqs } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { EmitData, EventController, EventControllerInterface } from '../event.controller'; +import { EventDto } from '../event.dto'; export class SqsController extends EventController implements EventControllerInterface { private sqs: SQS; @@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt return this.sqs; } + override async set(instanceName: string, data: EventDto): Promise { + if (!this.status) { + return; + } + + if (!data[this.name]?.enabled) { + data[this.name].events = []; + } else { + if (0 === data[this.name].events.length) { + data[this.name].events = EventController.events; + } + } + + await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled); + + const payload: any = { + where: { + instanceId: this.monitor.waInstances[instanceName].instanceId, + }, + update: { + enabled: data[this.name]?.enabled, + events: data[this.name].events, + }, + create: { + enabled: data[this.name]?.enabled, + events: data[this.name].events, + instanceId: this.monitor.waInstances[instanceName].instanceId, + }, + }; + console.log('*** payload: ', payload); + return this.prisma[this.name].upsert(payload); + } + public async emit({ instanceName, origin, @@ -121,70 +155,90 @@ export class SqsController extends EventController implements EventControllerInt } } - public async initQueues(instanceName: string, events: string[]) { - if (!events || !events.length) return; + private async saveQueues(instanceName: string, events: string[], enable: boolean) { + if (enable) { + const eventsFinded = await this.listQueuesByInstance(instanceName); + console.log('eventsFinded', eventsFinded); - const queues = events.map((event) => { - return `${event.replace(/_/g, '_').toLowerCase()}`; - }); + for (const event of events) { + const normalizedEvent = event.toLowerCase(); + if (eventsFinded.includes(normalizedEvent)) { + this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`); + continue; + } - queues.forEach((event) => { - const queueName = `${instanceName}_${event}.fifo`; + const queueName = `${instanceName}_${normalizedEvent}.fifo`; - this.sqs.createQueue( - { - QueueName: queueName, - Attributes: { - FifoQueue: 'true', - }, - }, - (err, data) => { - if (err) { - this.logger.error(`Error creating queue ${queueName}: ${err.message}`); - } else { - this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`); - } - }, - ); - }); + try { + const createCommand = new CreateQueueCommand({ + QueueName: queueName, + Attributes: { + FifoQueue: 'true', + }, + }); + const data = await this.sqs.send(createCommand); + this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`); + } catch (err: any) { + this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`); + } + } + } } - public async removeQueues(instanceName: string, events: any) { - const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : []; - if (!events || !eventsArray.length) return; + private async listQueuesByInstance(instanceName: string) { + let existingQueues: string[] = []; + try { + const listCommand = new ListQueuesCommand({ + QueueNamePrefix: `${instanceName}_`, + }); + const listData = await this.sqs.send(listCommand); + if (listData.QueueUrls && listData.QueueUrls.length > 0) { + // Extrai o nome da fila a partir da URL + existingQueues = listData.QueueUrls.map((queueUrl) => { + const parts = queueUrl.split('/'); + return parts[parts.length - 1]; + }); + } + } catch (error: any) { + this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`); + return; + } - const queues = eventsArray.map((event) => { - return `${event.replace(/_/g, '_').toLowerCase()}`; - }); + // Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo" + return existingQueues + .map((queueName) => { + // Espera-se que o nome seja `${instanceName}_${event}.fifo` + if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) { + return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase(); + } + return ''; + }) + .filter((event) => event !== ''); + } - queues.forEach((event) => { - const queueName = `${instanceName}_${event}.fifo`; + private async removeQueuesByInstance(instanceName: string) { + try { + const listCommand = new ListQueuesCommand({ + QueueNamePrefix: `${instanceName}_`, + }); + const listData = await this.sqs.send(listCommand); - this.sqs.getQueueUrl( - { - QueueName: queueName, - }, - (err, data) => { - if (err) { - this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`); - } else { - const queueUrl = data.QueueUrl; - - this.sqs.deleteQueue( - { - QueueUrl: queueUrl, - }, - (deleteErr) => { - if (deleteErr) { - this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`); - } else { - this.logger.info(`Queue ${queueName} deleted`); - } - }, - ); - } - }, - ); - }); + if (!listData.QueueUrls || listData.QueueUrls.length === 0) { + this.logger.info(`No queues found for instance ${instanceName}`); + return; + } + + for (const queueUrl of listData.QueueUrls) { + try { + const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl }); + await this.sqs.send(deleteCommand); + this.logger.info(`Queue ${queueUrl} deleted`); + } catch (err: any) { + this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`); + } + } + } catch (err: any) { + this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`); + } } } From 831b483e5edc0b0b1a1405b9ea70c44b94b6a5a5 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:04:44 -0300 Subject: [PATCH 14/30] =?UTF-8?q?=E2=9C=A8=20Remove=20reaction=20from=20a?= =?UTF-8?q?=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/sendMessage.controller.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index ac40562c..865fb6ba 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -18,6 +18,13 @@ import { WAMonitoringService } from '@api/services/monitor.service'; import { BadRequestException } from '@exceptions'; import { isBase64, isURL } from 'class-validator'; +function isEmoji(str: string) { + if (str === '') return true; + + const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u; + return emojiRegex.test(str); +} + export class SendMessageController { constructor(private readonly waMonitor: WAMonitoringService) {} @@ -81,8 +88,8 @@ export class SendMessageController { } public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { - if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) { - throw new BadRequestException('"reaction" must be an emoji'); + if (!isEmoji(data.reaction)) { + throw new BadRequestException('Reaction must be a single emoji or empty string'); } return await this.waMonitor.waInstances[instanceName].reactionMessage(data); } From 51edf1f8bae6ee39cb0fc4bf1aefaff06c938c7c Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:07:12 -0300 Subject: [PATCH 15/30] Fix: Linting requirements --- src/api/controllers/sendMessage.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index 865fb6ba..d730c3ab 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -21,7 +21,8 @@ import { isBase64, isURL } from 'class-validator'; function isEmoji(str: string) { if (str === '') return true; - const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u; + const emojiRegex = + /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u; return emojiRegex.test(str); } From ba4ea0bdc48e279358144ec4abfb9505ec08b04c Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:09:22 -0300 Subject: [PATCH 16/30] style: run lint --- src/api/controllers/sendMessage.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index d730c3ab..18339ce5 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -20,8 +20,8 @@ import { isBase64, isURL } from 'class-validator'; function isEmoji(str: string) { if (str === '') return true; - - const emojiRegex = + + const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u; return emojiRegex.test(str); } From 2e6bf88e27e13eb12f6ce82c805e2339c26423a5 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:15:18 -0300 Subject: [PATCH 17/30] feat: Add NATS integration and update Baileys service - Create Nats table in PostgreSQL migration - Disable message recovery logic in Baileys service - Remove console log in instance creation route --- .../whatsapp/whatsapp.baileys.service.ts | 26 +++++++++---------- src/api/routes/instance.router.ts | 1 - 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ddbb9944..376642a1 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -944,23 +944,23 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') { - this.logger.info(`Recovering message lost messageId: ${received.key.id}`); + //if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') { + // this.logger.info(`Recovering message lost messageId: ${received.key.id}`); - await this.baileysCache.set(received.key.id, { - message: received, - retry: 0, - }); + // await this.baileysCache.set(received.key.id, { + // message: received, + // retry: 0, + // }); - continue; - } + // continue; + //} - const retryCache = (await this.baileysCache.get(received.key.id)) || null; + // const retryCache = (await this.baileysCache.get(received.key.id)) || null; - if (retryCache) { - this.logger.info('Recovered message lost'); - await this.baileysCache.delete(received.key.id); - } + // if (retryCache) { + // this.logger.info('Recovered message lost'); + // await this.baileysCache.delete(received.key.id); + // } // Cache to avoid duplicate messages const messageKey = `${this.instance.id}_${received.key.id}`; diff --git a/src/api/routes/instance.router.ts b/src/api/routes/instance.router.ts index dd990c3b..3559893e 100644 --- a/src/api/routes/instance.router.ts +++ b/src/api/routes/instance.router.ts @@ -15,7 +15,6 @@ export class InstanceRouter extends RouterBroker { super(); this.router .post('/create', ...guards, async (req, res) => { - console.log('create instance', req.body); const response = await this.dataValidate({ request: req, schema: instanceSchema, From f12a18e9da2ec8aaab44a317e4677784ddf2bb6d Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:30:40 -0300 Subject: [PATCH 18/30] fix: adjustin cloud api send audio and video --- .../channel/meta/whatsapp.business.service.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 4ac7b8ac..12a3e5dd 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -750,6 +750,7 @@ export class BusinessStartupService extends ChannelStartupService { } if (message['media']) { const isImage = message['mimetype']?.startsWith('image/'); + const isVideo = message['mimetype']?.startsWith('video/'); content = { messaging_product: 'whatsapp', @@ -759,7 +760,7 @@ export class BusinessStartupService extends ChannelStartupService { [message['mediaType']]: { [message['type']]: message['id'], preview_url: linkPreview, - ...(message['fileName'] && !isImage && { filename: message['fileName'] }), + ...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }), caption: message['caption'], }, }; @@ -910,8 +911,9 @@ export class BusinessStartupService extends ChannelStartupService { private async getIdMedia(mediaMessage: any) { const formData = new FormData(); + const media = mediaMessage.media || mediaMessage.audio; - const fileStream = createReadStream(mediaMessage.media); + const fileStream = createReadStream(media); formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype }); formData.append('typeFile', mediaMessage.mimetype); @@ -1011,7 +1013,7 @@ export class BusinessStartupService extends ChannelStartupService { const prepareMedia: any = { fileName: `${hash}.mp3`, mediaType: 'audio', - media: audio, + audio, }; if (isURL(audio)) { @@ -1033,15 +1035,7 @@ export class BusinessStartupService extends ChannelStartupService { public async audioWhatsapp(data: SendAudioDto, file?: any) { const mediaData: SendAudioDto = { ...data }; - if (file?.buffer) { - mediaData.audio = file.buffer.toString('base64'); - } else if (isURL(mediaData.audio)) { - // DO NOTHING - // mediaData.audio = mediaData.audio; - } else { - console.error('El archivo no tiene buffer o file es undefined'); - throw new Error('File or buffer is undefined'); - } + if (file) mediaData.audio = file.buffer.toString('base64'); const message = await this.processAudio(mediaData.audio, data.number); From 780ca996171620176abff86bd2ac1eb7cecf7a1a Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:32:12 -0300 Subject: [PATCH 19/30] fix: adding media verification --- src/api/integrations/channel/meta/whatsapp.business.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 12a3e5dd..1f549467 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -912,6 +912,7 @@ export class BusinessStartupService extends ChannelStartupService { private async getIdMedia(mediaMessage: any) { const formData = new FormData(); const media = mediaMessage.media || mediaMessage.audio; + if (!media) throw new Error("Media or audio not found"); const fileStream = createReadStream(media); From bf1e936e5a9aaf736e561a13f4906bb1ee56d0bc Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:33:15 -0300 Subject: [PATCH 20/30] fix: lint --- src/api/integrations/channel/meta/whatsapp.business.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 1f549467..17e30950 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -912,7 +912,7 @@ export class BusinessStartupService extends ChannelStartupService { private async getIdMedia(mediaMessage: any) { const formData = new FormData(); const media = mediaMessage.media || mediaMessage.audio; - if (!media) throw new Error("Media or audio not found"); + if (!media) throw new Error('Media or audio not found'); const fileStream = createReadStream(media); From 659a140ba84d455cc93f180ef85383ea6b2bcfbe Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:50:44 -0300 Subject: [PATCH 21/30] feat: Enhance WebSocket authentication and connection handling - Add robust authentication mechanism for WebSocket connections - Implement API key validation for both instance-specific and global tokens - Improve connection request handling with detailed logging - Refactor WebSocket controller to support more secure connection validation --- .../event/websocket/websocket.controller.ts | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/api/integrations/event/websocket/websocket.controller.ts b/src/api/integrations/event/websocket/websocket.controller.ts index f6d152ff..76366f2d 100644 --- a/src/api/integrations/event/websocket/websocket.controller.ts +++ b/src/api/integrations/event/websocket/websocket.controller.ts @@ -1,6 +1,6 @@ import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { configService, Cors, Log, Websocket } from '@config/env.config'; +import { Auth, configService, Cors, Log, Websocket } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { Server } from 'http'; import { Server as SocketIO } from 'socket.io'; @@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl } this.socket = new SocketIO(httpServer, { - cors: { - origin: this.cors, + cors: { origin: this.cors }, + allowRequest: async (req, callback) => { + try { + const url = new URL(req.url || '', 'http://localhost'); + const params = new URLSearchParams(url.search); + + // Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4) + if (params.has('EIO')) { + return callback(null, true); + } + + const apiKey = params.get('apikey') || (req.headers.apikey as string); + + if (!apiKey) { + this.logger.error('Connection rejected: apiKey not provided'); + return callback('apiKey is required', false); + } + + const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } }); + + if (!instance) { + const globalToken = configService.get('AUTHENTICATION').API_KEY.KEY; + if (apiKey !== globalToken) { + this.logger.error('Connection rejected: invalid global token'); + return callback('Invalid global token', false); + } + } + + callback(null, true); + } catch (error) { + this.logger.error('Authentication error:'); + this.logger.error(error); + callback('Authentication error', false); + } }, }); @@ -101,10 +133,7 @@ export class WebsocketController extends EventController implements EventControl this.socket.emit(event, message); if (logEnabled) { - this.logger.log({ - local: `${origin}.sendData-WebsocketGlobal`, - ...message, - }); + this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message }); } } @@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl this.socket.of(`/${instanceName}`).emit(event, message); if (logEnabled) { - this.logger.log({ - local: `${origin}.sendData-Websocket`, - ...message, - }); + this.logger.log({ local: `${origin}.sendData-Websocket`, ...message }); } } } catch (err) { From bfd2cd637d7b044702461f0cf58df97d4dd4c73e Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Mon, 28 Apr 2025 15:54:55 -0300 Subject: [PATCH 22/30] basic regex for url --- src/api/integrations/event/webhook/webhook.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/event/webhook/webhook.controller.ts b/src/api/integrations/event/webhook/webhook.controller.ts index ce709c3d..7698d2de 100644 --- a/src/api/integrations/event/webhook/webhook.controller.ts +++ b/src/api/integrations/event/webhook/webhook.controller.ts @@ -6,7 +6,6 @@ import { configService, Log, Webhook } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; import axios, { AxiosInstance } from 'axios'; -import { isURL } from 'class-validator'; import { EmitData, EventController, EventControllerInterface } from '../event.controller'; @@ -18,7 +17,7 @@ export class WebhookController extends EventController implements EventControlle } override async set(instanceName: string, data: EventDto): Promise { - if (!isURL(data.webhook.url, { require_tld: false })) { + if (!/^(https?:\/\/)/.test(data.webhook.url)) { throw new BadRequestException('Invalid "url" property'); } @@ -78,6 +77,7 @@ export class WebhookController extends EventController implements EventControlle const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const enabledLog = configService.get('LOG').LEVEL.includes('WEBHOOKS'); + const regex = /^(https?:\/\/)/; const webhookData = { event, @@ -111,7 +111,7 @@ export class WebhookController extends EventController implements EventControlle } try { - if (instance?.enabled && isURL(instance.url, { require_tld: false })) { + if (instance?.enabled && regex.test(instance.url)) { const httpService = axios.create({ baseURL, headers: webhookHeaders as Record | undefined, @@ -155,7 +155,7 @@ export class WebhookController extends EventController implements EventControlle } try { - if (isURL(globalURL)) { + if (regex.test(globalURL)) { const httpService = axios.create({ baseURL: globalURL }); await this.retryWebhookRequest( From d85c80fbfd8e18d7d17d3f1adf2ac0a1053dbe53 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 13:11:34 -0300 Subject: [PATCH 23/30] Add eventos referente a instancia que estavam faltando --- src/api/integrations/event/event.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/integrations/event/event.controller.ts b/src/api/integrations/event/event.controller.ts index 2e6a2330..72c02132 100644 --- a/src/api/integrations/event/event.controller.ts +++ b/src/api/integrations/event/event.controller.ts @@ -151,5 +151,8 @@ export class EventController { 'TYPEBOT_CHANGE_STATUS', 'REMOVE_INSTANCE', 'LOGOUT_INSTANCE', + 'INSTANCE_CREATE', + 'INSTANCE_DELETE', + 'STATUS_INSTANCE', ]; } From 4440fceda6db333d42eae97b2b8e34bb16ce43c4 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 13:51:22 -0300 Subject: [PATCH 24/30] fix: preserve animation in GIF and WebP stickers --- .../whatsapp/whatsapp.baileys.service.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 376642a1..aa4b2279 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2348,15 +2348,36 @@ export class BaileysStartupService extends ChannelStartupService { imageBuffer = Buffer.from(response.data, 'binary'); } - const webpBuffer = await sharp(imageBuffer).webp().toBuffer(); - - return webpBuffer; + const isAnimated = image.includes('.gif') || + (image.includes('.webp') && this.isAnimatedWebp(imageBuffer)); + + if (isAnimated) { + return await sharp(imageBuffer, { animated: true }) + .webp({ quality: 80, animated: true }) + .toBuffer(); + } else { + return await sharp(imageBuffer).webp().toBuffer(); + } } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); throw error; } } + private isAnimatedWebp(buffer: Buffer): boolean { + if (buffer.length < 12) return false; + + for (let i = 0; i < buffer.length - 4; i++) { + if (buffer[i] === 0x41 && // 'A' + buffer[i + 1] === 0x4E && // 'N' + buffer[i + 2] === 0x49 && // 'I' + buffer[i + 3] === 0x4D) { // 'M' + return true; + } + } + return false; + } + public async mediaSticker(data: SendStickerDto, file?: any) { const mediaData: SendStickerDto = { ...data }; From 624a7d2e610a3033689bcf89f4a1fa95939459e2 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 13:54:51 -0300 Subject: [PATCH 25/30] fix: preserve animation in GIF and WebP stickers --- .../whatsapp/whatsapp.baileys.service.ts | 34 +++++----- .../event/websocket/websocket.controller.ts | 66 +++++++++---------- 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index aa4b2279..266f283a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2348,16 +2348,13 @@ export class BaileysStartupService extends ChannelStartupService { imageBuffer = Buffer.from(response.data, 'binary'); } - const isAnimated = image.includes('.gif') || - (image.includes('.webp') && this.isAnimatedWebp(imageBuffer)); - - if (isAnimated) { - return await sharp(imageBuffer, { animated: true }) - .webp({ quality: 80, animated: true }) - .toBuffer(); - } else { - return await sharp(imageBuffer).webp().toBuffer(); - } + const isAnimated = this.isAnimated(image, imageBuffer); + + if (isAnimated) { + return await sharp(imageBuffer, { animated: true }).webp({ quality: 80, animated: true }).toBuffer(); + } else { + return await sharp(imageBuffer).webp().toBuffer(); + } } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); throw error; @@ -2366,15 +2363,14 @@ export class BaileysStartupService extends ChannelStartupService { private isAnimatedWebp(buffer: Buffer): boolean { if (buffer.length < 12) return false; - - for (let i = 0; i < buffer.length - 4; i++) { - if (buffer[i] === 0x41 && // 'A' - buffer[i + 1] === 0x4E && // 'N' - buffer[i + 2] === 0x49 && // 'I' - buffer[i + 3] === 0x4D) { // 'M' - return true; - } - } + + return buffer.indexOf(Buffer.from('ANIM')) !== -1; + } + + private isAnimated(image: string, buffer: Buffer): boolean { + if (image.includes('.gif')) return true; + + if (image.includes('.webp')) return this.isAnimatedWebp(buffer); return false; } diff --git a/src/api/integrations/event/websocket/websocket.controller.ts b/src/api/integrations/event/websocket/websocket.controller.ts index 76366f2d..a1cef2db 100644 --- a/src/api/integrations/event/websocket/websocket.controller.ts +++ b/src/api/integrations/event/websocket/websocket.controller.ts @@ -25,39 +25,39 @@ export class WebsocketController extends EventController implements EventControl this.socket = new SocketIO(httpServer, { cors: { origin: this.cors }, - allowRequest: async (req, callback) => { - try { - const url = new URL(req.url || '', 'http://localhost'); - const params = new URLSearchParams(url.search); - - // Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4) - if (params.has('EIO')) { - return callback(null, true); - } - - const apiKey = params.get('apikey') || (req.headers.apikey as string); - - if (!apiKey) { - this.logger.error('Connection rejected: apiKey not provided'); - return callback('apiKey is required', false); - } - - const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } }); - - if (!instance) { - const globalToken = configService.get('AUTHENTICATION').API_KEY.KEY; - if (apiKey !== globalToken) { - this.logger.error('Connection rejected: invalid global token'); - return callback('Invalid global token', false); - } - } - - callback(null, true); - } catch (error) { - this.logger.error('Authentication error:'); - this.logger.error(error); - callback('Authentication error', false); - } + allowRequest: async (req, callback) => { + try { + const url = new URL(req.url || '', 'http://localhost'); + const params = new URLSearchParams(url.search); + + // Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4) + if (params.has('EIO')) { + return callback(null, true); + } + + const apiKey = params.get('apikey') || (req.headers.apikey as string); + + if (!apiKey) { + this.logger.error('Connection rejected: apiKey not provided'); + return callback('apiKey is required', false); + } + + const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } }); + + if (!instance) { + const globalToken = configService.get('AUTHENTICATION').API_KEY.KEY; + if (apiKey !== globalToken) { + this.logger.error('Connection rejected: invalid global token'); + return callback('Invalid global token', false); + } + } + + callback(null, true); + } catch (error) { + this.logger.error('Authentication error:'); + this.logger.error(error); + callback('Authentication error', false); + } }, }); From 82ea7bd224d7de7d04e4d72d4a414972c714ec9f Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 13:57:17 -0300 Subject: [PATCH 26/30] fix: normalize file extension checks for case insensitivity in sticker conversion --- .../channel/whatsapp/whatsapp.baileys.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 266f283a..6deb8196 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2351,7 +2351,7 @@ export class BaileysStartupService extends ChannelStartupService { const isAnimated = this.isAnimated(image, imageBuffer); if (isAnimated) { - return await sharp(imageBuffer, { animated: true }).webp({ quality: 80, animated: true }).toBuffer(); + return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer(); } else { return await sharp(imageBuffer).webp().toBuffer(); } @@ -2368,9 +2368,11 @@ export class BaileysStartupService extends ChannelStartupService { } private isAnimated(image: string, buffer: Buffer): boolean { - if (image.includes('.gif')) return true; + const lowerCaseImage = image.toLowerCase(); - if (image.includes('.webp')) return this.isAnimatedWebp(buffer); + if (lowerCaseImage.includes('.gif')) return true; + + if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer); return false; } From 9c4ee35dc9edd9395ec07d95b70259a566410965 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 14:13:41 -0300 Subject: [PATCH 27/30] fix_and_add_name_to_find_chats_and_paginate_get_contacts_and_get_chats --- .../whatsapp/whatsapp.baileys.service.ts | 5 +++-- src/api/services/channel.service.ts | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 6deb8196..721cc7de 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -998,7 +998,8 @@ export class BaileysStartupService extends ChannelStartupService { existingChat && received.pushName && existingChat.name !== received.pushName && - received.pushName.trim().length > 0 + received.pushName.trim().length > 0 && + !received.key.remoteJid.includes('@g.us') ) { this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]); if (this.configService.get('DATABASE').SAVE_DATA.CHATS) { @@ -2371,7 +2372,7 @@ export class BaileysStartupService extends ChannelStartupService { const lowerCaseImage = image.toLowerCase(); if (lowerCaseImage.includes('.gif')) return true; - + if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer); return false; } diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 66c59679..1ce81602 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -330,9 +330,14 @@ export class ChannelStartupService { where['remoteJid'] = remoteJid; } - return await this.prismaRepository.contact.findMany({ + const contactFindManyArgs: Prisma.ContactFindManyArgs = { where, - }); + }; + + if (query.offset) contactFindManyArgs.take = query.offset; + if (query.page) contactFindManyArgs.skip = query.offset * ((query.page as number) - 1); + + return await this.prismaRepository.contact.findMany(contactFindManyArgs); } public cleanMessageData(message: any) { @@ -501,6 +506,13 @@ export class ChannelStartupService { : createJid(query.where?.remoteJid) : null; + const limit = + query.offset && !query.page + ? Prisma.sql` LIMIT ${query.offset}` + : query.offset && query.page + ? Prisma.sql` LIMIT ${query.offset} OFFSET ${((query.page as number) - 1) * query.offset}` + : Prisma.sql``; + const where = { instanceId: this.instanceId, }; @@ -527,6 +539,7 @@ export class ChannelStartupService { to_timestamp("Message"."messageTimestamp"::double precision), "Contact"."updatedAt" ) as "updatedAt", + "Chat"."name" as "chatName", "Chat"."createdAt" as "windowStart", "Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires", CASE @@ -557,6 +570,7 @@ export class ChannelStartupService { ORDER BY "Contact"."remoteJid", "Message"."messageTimestamp" DESC + ${limit} ) SELECT * FROM rankedMessages ORDER BY "updatedAt" DESC NULLS LAST; @@ -585,6 +599,7 @@ export class ChannelStartupService { id: contact.id, remoteJid: contact.remoteJid, pushName: contact.pushName, + chatName: contact.chatName, profilePicUrl: contact.profilePicUrl, updatedAt: contact.updatedAt, windowStart: contact.windowStart, From 01368153d1ecaf1ac4f6db0467492ce7a8e58025 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 14:18:29 -0300 Subject: [PATCH 28/30] Update src/api/services/channel.service.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/api/services/channel.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 1ce81602..778fa0e8 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -335,7 +335,10 @@ export class ChannelStartupService { }; if (query.offset) contactFindManyArgs.take = query.offset; - if (query.page) contactFindManyArgs.skip = query.offset * ((query.page as number) - 1); + if (query.page) { + const validPage = Math.max(query.page as number, 1); + contactFindManyArgs.skip = query.offset * (validPage - 1); + } return await this.prismaRepository.contact.findMany(contactFindManyArgs); } From 5acd7a977789aed84b60b697aa0edcfc88234413 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 14:24:26 -0300 Subject: [PATCH 29/30] [FIX] Adding a timestamp to the filename to make it possible to send the same file more than once in the same conversation. --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 721cc7de..3806371e 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1068,7 +1068,7 @@ export class BaileysStartupService extends ChannelStartupService { const { buffer, mediaType, fileName, size } = media; const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName); + const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, `${Date.now()}_${fileName}`); await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, }); From a547c12ce8be5ca22cff65f9d4ea51d843de95e7 Mon Sep 17 00:00:00 2001 From: matheusmartinsInsper Date: Wed, 30 Apr 2025 14:29:26 -0300 Subject: [PATCH 30/30] [FIX] Run lint --- .../channel/whatsapp/whatsapp.baileys.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3806371e..49e1e501 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1068,7 +1068,12 @@ export class BaileysStartupService extends ChannelStartupService { const { buffer, mediaType, fileName, size } = media; const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, `${Date.now()}_${fileName}`); + const fullName = join( + `${this.instance.id}`, + received.key.remoteJid, + mediaType, + `${Date.now()}_${fileName}`, + ); await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, });