From 52fa931140b2e998bd76a72d3cfdf00fabf2ce1b Mon Sep 17 00:00:00 2001 From: Caio Bleggi Date: Tue, 9 Dec 2025 12:03:47 -0300 Subject: [PATCH 01/71] feat(channel): add support for @newsletter in sendMessage and findChannels --- src/api/controllers/chat.controller.ts | 4 ++ .../whatsapp/whatsapp.baileys.service.ts | 68 ++++++++++++++++++- src/api/routes/chat.router.ts | 10 +++ src/utils/createJid.ts | 7 +- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 22e90b9fa..6a21fa56a 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -113,4 +113,8 @@ export class ChatController { public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) { return await this.waMonitor.waInstances[instanceName].blockUser(data); } + + public async fetchChannels({ instanceName }: InstanceDto, query: Query) { + return await this.waMonitor.waInstances[instanceName].fetchChannels(query); + } } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..db743facf 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3511,9 +3511,24 @@ export class BaileysStartupService extends ChannelStartupService { users: { number: string; jid: string; name?: string }[]; } = { groups: [], broadcast: [], users: [] }; + const onWhatsapp: OnWhatsAppDto[] = []; + data.numbers.forEach((number) => { const jid = createJid(number); + if (isJidNewsletter(jid)) { + onWhatsapp.push( + new OnWhatsAppDto( + jid, + true, // Newsletters are always valid + number, + undefined, // Can be fetched later if needed + 'newsletter', // Indicate it's a newsletter type + ), + ); + return; + } + if (isJidGroup(jid)) { jids.groups.push({ number, jid }); } else if (jid === 'status@broadcast') { @@ -3523,8 +3538,6 @@ export class BaileysStartupService extends ChannelStartupService { } }); - const onWhatsapp: OnWhatsAppDto[] = []; - // BROADCAST onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number))); @@ -4700,6 +4713,10 @@ export class BaileysStartupService extends ChannelStartupService { } } + if (isJidNewsletter(message.key.remoteJid) && message.key.fromMe) { + messageRaw.status = status[3]; // DELIVERED MESSAGE TO NEWSLETTER CHANNEL + } + return messageRaw; } @@ -5119,4 +5136,51 @@ export class BaileysStartupService extends ChannelStartupService { }, }; } + public async fetchChannels(query: Query) { + const page = Number((query as any)?.page ?? 1); + const limit = Number((query as any)?.limit ?? (query as any)?.rows ?? 50); + const skip = (page - 1) * limit; + + const messages = await this.prismaRepository.message.findMany({ + where: { + instanceId: this.instanceId, + AND: [{ key: { path: ['remoteJid'], not: null } }], + }, + orderBy: { messageTimestamp: 'desc' }, + select: { + key: true, + messageTimestamp: true, + }, + }); + + const channelMap = new Map(); + + for (const msg of messages) { + const key = msg.key as any; + const remoteJid = key?.remoteJid as string | undefined; + if (!remoteJid || !isJidNewsletter(remoteJid)) continue; + + if (!channelMap.has(remoteJid)) { + channelMap.set(remoteJid, { + remoteJid, + pushName: undefined, // Push name is never stored for channels, so we set it as undefined + lastMessageTimestamp: msg.messageTimestamp, + }); + } + } + + const allChannels = Array.from(channelMap.values()); + + const total = allChannels.length; + const pages = Math.ceil(total / limit); + const records = allChannels.slice(skip, skip + limit); + + return { + total, + pages, + currentPage: page, + limit, + records, + }; + } } diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 158947ed2..f372950fd 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -281,6 +281,16 @@ export class ChatRouter extends RouterBroker { }); return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('findChannels'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: contactValidateSchema, + ClassRef: Query, + execute: (instance, query) => chatController.fetchChannels(instance, query as any), + }); + + return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/utils/createJid.ts b/src/utils/createJid.ts index a680e821e..23a3afe1f 100644 --- a/src/utils/createJid.ts +++ b/src/utils/createJid.ts @@ -35,7 +35,12 @@ function formatBRNumber(jid: string) { export function createJid(number: string): string { number = number.replace(/:\d+/, ''); - if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { + if ( + number.includes('@g.us') || + number.includes('@s.whatsapp.net') || + number.includes('@lid') || + number.includes('@newsletter') + ) { return number; } From 5faf3d18d6db5566048c4245a846528c2481f8e8 Mon Sep 17 00:00:00 2001 From: OrionDesign Date: Tue, 9 Dec 2025 16:56:46 -0300 Subject: [PATCH 02/71] Add poll vote decryption endpoint and logic Introduces a new API endpoint and supporting logic to decrypt WhatsApp poll votes. Adds DecryptPollVoteDto, validation schema, controller method, and service logic to process and aggregate poll vote results based on poll creation message key. --- src/api/controllers/chat.controller.ts | 5 + src/api/dto/chat.dto.ts | 9 + .../whatsapp/whatsapp.baileys.service.ts | 243 ++++++++++++++++++ src/api/routes/chat.router.ts | 12 + src/validate/message.schema.ts | 18 ++ 5 files changed, 287 insertions(+) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 22e90b9fa..e1d2458f9 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -1,6 +1,7 @@ import { ArchiveChatDto, BlockUserDto, + DecryptPollVoteDto, DeleteMessage, getBase64FromMediaMessageDto, MarkChatUnreadDto, @@ -113,4 +114,8 @@ export class ChatController { public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) { return await this.waMonitor.waInstances[instanceName].blockUser(data); } + + public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) { + return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(data.pollCreationMessageKey); + } } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index b11f32b05..1e6bcbcfc 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -127,3 +127,12 @@ export class BlockUserDto { number: string; status: 'block' | 'unblock'; } + +export class DecryptPollVoteDto { + pollCreationMessageKey: { + id: string; + remoteJid: string; + participant?: string; + fromMe?: boolean; + }; +} \ No newline at end of file diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..1b678ad91 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -5119,4 +5119,247 @@ export class BaileysStartupService extends ChannelStartupService { }, }; } + + public async baileysDecryptPollVote(pollCreationMessageKey: proto.IMessageKey) { + try { + this.logger.verbose('Starting poll vote decryption process'); + + // Buscar a mensagem de criação da enquete + const pollCreationMessage = (await this.getMessage(pollCreationMessageKey, true)) as proto.IWebMessageInfo; + + if (!pollCreationMessage) { + throw new NotFoundException('Poll creation message not found'); + } + + // Extrair opções da enquete + const pollOptions = + (pollCreationMessage.message as any)?.pollCreationMessage?.options || + (pollCreationMessage.message as any)?.pollCreationMessageV3?.options || + []; + + if (!pollOptions || pollOptions.length === 0) { + throw new NotFoundException('Poll options not found'); + } + + // Recuperar chave de criptografia + const pollMessageSecret = (await this.getMessage(pollCreationMessageKey)) as any; + let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret; + + if (!pollEncKey) { + throw new NotFoundException('Poll encryption key not found'); + } + + // Normalizar chave de criptografia + if (typeof pollEncKey === 'string') { + pollEncKey = Buffer.from(pollEncKey, 'base64'); + } else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) { + pollEncKey = Buffer.from(pollEncKey.data); + } + + if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) { + pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64'); + } + + // Buscar todas as mensagens de atualização de votos + const allPollUpdateMessages = await this.prismaRepository.message.findMany({ + where: { + instanceId: this.instanceId, + messageType: 'pollUpdateMessage', + }, + select: { + id: true, + key: true, + message: true, + messageTimestamp: true, + }, + }); + + this.logger.verbose(`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database`); + + // Filtrar apenas mensagens relacionadas a esta enquete específica + const pollUpdateMessages = allPollUpdateMessages.filter((msg) => { + const pollUpdate = (msg.message as any)?.pollUpdateMessage; + if (!pollUpdate) return false; + + const creationKey = pollUpdate.pollCreationMessageKey; + if (!creationKey) return false; + + return ( + creationKey.id === pollCreationMessageKey.id && + jidNormalizedUser(creationKey.remoteJid || '') === jidNormalizedUser(pollCreationMessageKey.remoteJid || '') + ); + }); + + this.logger.verbose(`Filtered to ${pollUpdateMessages.length} matching poll update messages`); + + // Preparar candidatos de JID para descriptografia + const creatorCandidates = [ + this.instance.wuid, + this.client.user?.lid, + pollCreationMessage.key.participant, + (pollCreationMessage.key as any).participantAlt, + pollCreationMessage.key.remoteJid, + (pollCreationMessage.key as any).remoteJidAlt, + ].filter(Boolean); + + const uniqueCreators = [...new Set(creatorCandidates.map((id) => jidNormalizedUser(id)))]; + + // Processar votos + const votesByUser = new Map(); + + this.logger.verbose(`Processing ${pollUpdateMessages.length} poll update messages for decryption`); + + for (const pollUpdateMsg of pollUpdateMessages) { + const pollVote = (pollUpdateMsg.message as any)?.pollUpdateMessage?.vote; + if (!pollVote) continue; + + const key = pollUpdateMsg.key as any; + const voterCandidates = [ + this.instance.wuid, + this.client.user?.lid, + key.participant, + key.participantAlt, + key.remoteJidAlt, + key.remoteJid, + ].filter(Boolean); + + const uniqueVoters = [...new Set(voterCandidates.map((id) => jidNormalizedUser(id)))]; + + let selectedOptionNames: string[] = []; + let successfulVoterJid: string | undefined; + + // Verificar se o voto já está descriptografado + if (pollVote.selectedOptions && Array.isArray(pollVote.selectedOptions)) { + const selectedOptions = pollVote.selectedOptions; + this.logger.verbose('Vote already has selectedOptions, checking format'); + + // Verificar se são strings (já descriptografado) ou buffers (precisa descriptografar) + if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') { + // Já está descriptografado como nomes de opções + selectedOptionNames = selectedOptions; + successfulVoterJid = uniqueVoters[0]; + this.logger.verbose(`Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`); + } else { + // Está como hash, precisa converter para nomes + selectedOptionNames = pollOptions + .filter((option: any) => { + const hash = createHash('sha256').update(option.optionName).digest(); + return selectedOptions.some((selected: any) => { + if (Buffer.isBuffer(selected)) { + return Buffer.compare(selected, hash) === 0; + } + return false; + }); + }) + .map((option: any) => option.optionName); + successfulVoterJid = uniqueVoters[0]; + } + } else if (pollVote.encPayload && pollEncKey) { + // Tentar descriptografar + let decryptedVote: any = null; + + for (const creator of uniqueCreators) { + for (const voter of uniqueVoters) { + try { + decryptedVote = decryptPollVote(pollVote, { + pollCreatorJid: creator, + pollMsgId: pollCreationMessage.key.id, + pollEncKey, + voterJid: voter, + } as any); + + if (decryptedVote) { + successfulVoterJid = voter; + break; + } + } catch (error) { + // Continue tentando outras combinações + } + } + if (decryptedVote) break; + } + + if (decryptedVote && decryptedVote.selectedOptions) { + // Converter hashes para nomes de opções + selectedOptionNames = pollOptions + .filter((option: any) => { + const hash = createHash('sha256').update(option.optionName).digest(); + return decryptedVote.selectedOptions.some((selected: any) => { + if (Buffer.isBuffer(selected)) { + return Buffer.compare(selected, hash) === 0; + } + return false; + }); + }) + .map((option: any) => option.optionName); + + this.logger.verbose(`Successfully decrypted vote for voter: ${successfulVoterJid}, creator: ${uniqueCreators[0]}`); + } else { + this.logger.warn(`Failed to decrypt vote. Last error: Could not decrypt with any combination`); + continue; + } + } else { + this.logger.warn('Vote has no encPayload and no selectedOptions, skipping'); + continue; + } + + if (selectedOptionNames.length > 0 && successfulVoterJid) { + const normalizedVoterJid = jidNormalizedUser(successfulVoterJid); + const existingVote = votesByUser.get(normalizedVoterJid); + + // Manter apenas o voto mais recente de cada usuário + if (!existingVote || pollUpdateMsg.messageTimestamp > existingVote.timestamp) { + votesByUser.set(normalizedVoterJid, { + timestamp: pollUpdateMsg.messageTimestamp, + selectedOptions: selectedOptionNames, + voterJid: successfulVoterJid, + }); + } + } + } + + // Agrupar votos por opção + const results: Record = {}; + + // Inicializar todas as opções com zero votos + pollOptions.forEach((option: any) => { + results[option.optionName] = { + votes: 0, + voters: [], + }; + }); + + // Agregar votos + votesByUser.forEach((voteData) => { + voteData.selectedOptions.forEach((optionName) => { + if (results[optionName]) { + results[optionName].votes++; + if (!results[optionName].voters.includes(voteData.voterJid)) { + results[optionName].voters.push(voteData.voterJid); + } + } + }); + }); + + // Obter nome da enquete + const pollName = + (pollCreationMessage.message as any)?.pollCreationMessage?.name || + (pollCreationMessage.message as any)?.pollCreationMessageV3?.name || + 'Enquete sem nome'; + + // Calcular total de votos únicos + const totalVotes = votesByUser.size; + + return { + poll: { + name: pollName, + totalVotes, + results, + }, + }; + } catch (error) { + this.logger.error(`Error decrypting poll votes: ${error}`); + throw new InternalServerErrorException('Error decrypting poll votes', error.toString()); + } + } } diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 158947ed2..e374b6d63 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -2,6 +2,7 @@ import { RouterBroker } from '@api/abstract/abstract.router'; import { ArchiveChatDto, BlockUserDto, + DecryptPollVoteDto, DeleteMessage, getBase64FromMediaMessageDto, MarkChatUnreadDto, @@ -23,6 +24,7 @@ import { archiveChatSchema, blockUserSchema, contactValidateSchema, + decryptPollVoteSchema, deleteMessageSchema, markChatUnreadSchema, messageUpSchema, @@ -281,6 +283,16 @@ export class ChatRouter extends RouterBroker { }); return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('getPollVote'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: decryptPollVoteSchema, + ClassRef: DecryptPollVoteDto, + execute: (instance, data) => chatController.decryptPollVote(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index d514c6199..79d5cda2c 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -447,3 +447,21 @@ export const buttonsMessageSchema: JSONSchema7 = { }, required: ['number'], }; + +export const decryptPollVoteSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + pollCreationMessageKey: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + participant: { type: 'string' }, + fromMe: { type: 'boolean' }, + }, + required: ['id', 'remoteJid'], + }, + }, + required: ['pollCreationMessageKey'], +}; \ No newline at end of file From 076449e5d6ab5a867988ac3d9bd6f3117d5816be Mon Sep 17 00:00:00 2001 From: OrionDesign Date: Tue, 9 Dec 2025 17:03:15 -0300 Subject: [PATCH 03/71] Refactor DecryptPollVoteDto and schema structure Updated DecryptPollVoteDto to use a nested message.key structure and moved remoteJid to the top level. Adjusted the controller and validation schema to match the new structure for consistency and clarity. --- src/api/controllers/chat.controller.ts | 6 +++++- src/api/dto/chat.dto.ts | 10 +++++----- src/validate/message.schema.ts | 18 +++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index e1d2458f9..c224ef927 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -116,6 +116,10 @@ export class ChatController { } public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) { - return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(data.pollCreationMessageKey); + const pollCreationMessageKey = { + id: data.message.key.id, + remoteJid: data.remoteJid, + }; + return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey); } } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 1e6bcbcfc..a8098729f 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -129,10 +129,10 @@ export class BlockUserDto { } export class DecryptPollVoteDto { - pollCreationMessageKey: { - id: string; - remoteJid: string; - participant?: string; - fromMe?: boolean; + message: { + key: { + id: string; + }; }; + remoteJid: string; } \ No newline at end of file diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index 79d5cda2c..aef922cd9 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -452,16 +452,20 @@ export const decryptPollVoteSchema: JSONSchema7 = { $id: v4(), type: 'object', properties: { - pollCreationMessageKey: { + message: { type: 'object', properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - participant: { type: 'string' }, - fromMe: { type: 'boolean' }, + key: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, }, - required: ['id', 'remoteJid'], + required: ['key'], }, + remoteJid: { type: 'string' }, }, - required: ['pollCreationMessageKey'], + required: ['message', 'remoteJid'], }; \ No newline at end of file From 67c4aa640b726467810cefbeb0482fe882a47d6d Mon Sep 17 00:00:00 2001 From: OrionDesign Date: Thu, 11 Dec 2025 17:01:13 -0300 Subject: [PATCH 04/71] =?UTF-8?q?refactor(baileys):=20atualizar=20servi?= =?UTF-8?q?=C3=83=C2=A7o=20de=20mensagens=20e=20schemas=20de=20valida?= =?UTF-8?q?=C3=83=C2=A7=C3=83=C2=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 28 +++++ src/api/dto/chat.dto.ts | 2 +- .../whatsapp/whatsapp.baileys.service.ts | 116 +++++++++--------- src/validate/message.schema.ts | 2 +- 4 files changed, 90 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index c45e8fef3..1aafeaba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2907,6 +2907,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2928,6 +2929,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2940,6 +2942,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2955,6 +2958,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz", "integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.204.0", "import-in-the-middle": "^1.8.1", @@ -3362,6 +3366,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3378,6 +3383,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -3395,6 +3401,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3643,6 +3650,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4933,6 +4941,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5108,6 +5117,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -5411,6 +5421,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5758,6 +5769,7 @@ "resolved": "https://registry.npmjs.org/audio-decode/-/audio-decode-2.2.3.tgz", "integrity": "sha512-Z0lHvMayR/Pad9+O9ddzaBJE0DrhZkQlStrC1RwcAHF3AhQAsdwKHeLGK8fYKyp2DDU6xHxzGb4CLMui12yVrg==", "license": "MIT", + "peer": true, "dependencies": { "@wasm-audio-decoders/flac": "^0.2.4", "@wasm-audio-decoders/ogg-vorbis": "^0.1.15", @@ -6746,6 +6758,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7636,6 +7649,7 @@ "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7706,6 +7720,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7762,6 +7777,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8368,6 +8384,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -10355,6 +10372,7 @@ "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", "integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", @@ -10585,6 +10603,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz", "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -10680,6 +10699,7 @@ "resolved": "https://registry.npmjs.org/link-preview-js/-/link-preview-js-3.2.0.tgz", "integrity": "sha512-FvrLltjOPGbTzt+RugbzM7g8XuUNLPO2U/INSLczrYdAA32E7nZVUrVL1gr61DGOArGJA2QkPGMEvNMLLsXREA==", "license": "MIT", + "peer": true, "dependencies": { "cheerio": "1.0.0-rc.11", "url": "0.11.0" @@ -12600,6 +12620,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -12909,6 +12930,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12938,6 +12960,7 @@ "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.0", "@prisma/engines": "6.19.0" @@ -14029,6 +14052,7 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14871,6 +14895,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15059,6 +15084,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -15707,6 +15733,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16222,6 +16249,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "devOptional": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index a8098729f..aeaab6f84 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -135,4 +135,4 @@ export class DecryptPollVoteDto { }; }; remoteJid: string; -} \ No newline at end of file +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 1b678ad91..819138474 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -733,7 +733,7 @@ export class BaileysStartupService extends ChannelStartupService { }); return await this.createClient(number); - } catch (error) { + } catch { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } @@ -742,7 +742,7 @@ export class BaileysStartupService extends ChannelStartupService { public async reloadConnection(): Promise { try { return await this.createClient(this.phoneNumber); - } catch (error) { + } catch { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } @@ -888,7 +888,7 @@ export class BaileysStartupService extends ChannelStartupService { }), ); } - } catch (error) { + } catch { console.error(error); this.logger.error(`Error: ${error.message}`); } @@ -1074,7 +1074,7 @@ export class BaileysStartupService extends ChannelStartupService { contacts = undefined; messages = undefined; chats = undefined; - } catch (error) { + } catch { this.logger.error(error); } }, @@ -1434,7 +1434,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } - } catch (error) { + } catch { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } } @@ -1466,7 +1466,7 @@ export class BaileysStartupService extends ChannelStartupService { messageRaw.message.base64 = buffer.toString('base64'); } } - } catch (error) { + } catch { this.logger.error(['Error converting media to base64', error?.message]); } } @@ -1550,7 +1550,7 @@ export class BaileysStartupService extends ChannelStartupService { create: contactRaw, }); } - } catch (error) { + } catch { this.logger.error(error); } }, @@ -1798,7 +1798,7 @@ export class BaileysStartupService extends ChannelStartupService { }; this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate); - } catch (error) { + } catch { this.logger.error( `Failed to resolve participant data for GROUP_PARTICIPANTS_UPDATE webhook: ${error.message} | Group: ${participantsUpdate.id} | Participants: ${participantsUpdate.participants.length}`, ); @@ -2005,7 +2005,7 @@ export class BaileysStartupService extends ChannelStartupService { return; } } - } catch (error) { + } catch { this.logger.error(error); } }); @@ -2122,7 +2122,7 @@ export class BaileysStartupService extends ChannelStartupService { // return call; return { id: '123', jid, isVideo, callDuration }; - } catch (error) { + } catch { return error; } } @@ -2503,7 +2503,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } - } catch (error) { + } catch { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } } @@ -2534,7 +2534,7 @@ export class BaileysStartupService extends ChannelStartupService { messageRaw.message.base64 = buffer.toString('base64'); } } - } catch (error) { + } catch { this.logger.error(['Error converting media to base64', error?.message]); } } @@ -2555,7 +2555,7 @@ export class BaileysStartupService extends ChannelStartupService { } return messageRaw; - } catch (error) { + } catch { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2607,7 +2607,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { presence: data.presence }; - } catch (error) { + } catch { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2619,7 +2619,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.sendPresenceUpdate(data.presence); return { presence: data.presence }; - } catch (error) { + } catch { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2860,7 +2860,7 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(`Video duration: ${duration} seconds`); prepareMedia[mediaType].seconds = duration; - } catch (error) { + } catch { this.logger.error('Error getting video duration:'); this.logger.error(error); throw new Error(`Failed to get video duration: ${error.message}`); @@ -2887,7 +2887,7 @@ export class BaileysStartupService extends ChannelStartupService { { [mediaType]: { ...prepareMedia[mediaType] } }, { userJid: this.instance.wuid }, ); - } catch (error) { + } catch { this.logger.error(error); throw new InternalServerErrorException(error?.toString() || error); } @@ -2932,7 +2932,7 @@ export class BaileysStartupService extends ChannelStartupService { } else { return await sharp(imageBuffer).webp().toBuffer(); } - } catch (error) { + } catch { console.error('Erro ao converter a imagem para WebP:', error); throw error; } @@ -3683,7 +3683,7 @@ export class BaileysStartupService extends ChannelStartupService { }); await this.client.readMessages(keys); return { message: 'Read messages', read: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Read messages fail', error.toString()); } } @@ -3732,7 +3732,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.chatModify({ archive: data.archive, lastMessages: [last_message] }, createJid(number)); return { chatId: number, archived: true }; - } catch (error) { + } catch { throw new InternalServerErrorException({ archived: false, message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], @@ -3760,7 +3760,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.chatModify({ markRead: false, lastMessages: [last_message] }, createJid(number)); return { chatId: number, markedChatUnread: true }; - } catch (error) { + } catch { throw new InternalServerErrorException({ markedChatUnread: false, message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()], @@ -3817,7 +3817,7 @@ export class BaileysStartupService extends ChannelStartupService { } return response; - } catch (error) { + } catch { throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); } } @@ -3955,7 +3955,7 @@ export class BaileysStartupService extends ChannelStartupService { return result; } - } catch (error) { + } catch { this.logger.error('Error converting audio to mp4:'); this.logger.error(error); throw new BadRequestException('Failed to convert audio to MP4'); @@ -3971,7 +3971,7 @@ export class BaileysStartupService extends ChannelStartupService { base64: buffer.toString('base64'), buffer: getBuffer ? buffer : null, }; - } catch (error) { + } catch { this.logger.error('Error processing media message:'); this.logger.error(error); throw new BadRequestException(error.toString()); @@ -4013,7 +4013,7 @@ export class BaileysStartupService extends ChannelStartupService { groupadd: settings.groupadd, }, }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating privacy settings', error.toString()); } } @@ -4031,7 +4031,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { isBusiness: true, ...profile }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } @@ -4041,7 +4041,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfileName(name); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } @@ -4051,7 +4051,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfileStatus(status); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating profile status', error.toString()); } } @@ -4092,7 +4092,7 @@ export class BaileysStartupService extends ChannelStartupService { this.reloadConnection(); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating profile picture', error.toString()); } } @@ -4104,7 +4104,7 @@ export class BaileysStartupService extends ChannelStartupService { this.reloadConnection(); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error removing profile picture', error.toString()); } } @@ -4124,7 +4124,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateBlockStatus(sender, data.status); return { block: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error blocking user', error.toString()); } } @@ -4150,7 +4150,7 @@ export class BaileysStartupService extends ChannelStartupService { } return null; - } catch (error) { + } catch { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -4238,7 +4238,7 @@ export class BaileysStartupService extends ChannelStartupService { } return messageSent; - } catch (error) { + } catch { this.logger.error(error); throw error; } @@ -4278,7 +4278,7 @@ export class BaileysStartupService extends ChannelStartupService { return { numberJid: contact.jid, labelId: data.labelId, remove: true }; } - } catch (error) { + } catch { throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString()); } } @@ -4296,7 +4296,7 @@ export class BaileysStartupService extends ChannelStartupService { } return meta; - } catch (error) { + } catch { this.logger.error(error); return null; } @@ -4344,7 +4344,7 @@ export class BaileysStartupService extends ChannelStartupService { const group = await this.client.groupMetadata(id); return group; - } catch (error) { + } catch { this.logger.error(error); throw new InternalServerErrorException('Error creating group', error.toString()); } @@ -4383,7 +4383,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfilePicture(picture.groupJid, pic); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error update group picture', error.toString()); } } @@ -4393,7 +4393,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.groupUpdateSubject(data.groupJid, data.subject); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating group subject', error.toString()); } } @@ -4403,7 +4403,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.groupUpdateDescription(data.groupJid, data.description); return { update: 'success' }; - } catch (error) { + } catch { throw new InternalServerErrorException('Error updating group description', error.toString()); } } @@ -4437,7 +4437,7 @@ export class BaileysStartupService extends ChannelStartupService { isCommunityAnnounce: group.isCommunityAnnounce, linkedParent: group.linkedParent, }; - } catch (error) { + } catch { if (reply === 'inner') { return; } @@ -4484,7 +4484,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const code = await this.client.groupInviteCode(id.groupJid); return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { + } catch { throw new NotFoundException('No invite code', error.toString()); } } @@ -4524,7 +4524,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const groupJid = await this.client.groupAcceptInvite(id.inviteCode); return { accepted: true, groupJid: groupJid }; - } catch (error) { + } catch { throw new NotFoundException('Accept invite error', error.toString()); } } @@ -4533,7 +4533,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const inviteCode = await this.client.groupRevokeInvite(id.groupJid); return { revoked: true, inviteCode }; - } catch (error) { + } catch { throw new NotFoundException('Revoke error', error.toString()); } } @@ -4559,7 +4559,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { participants: parsedParticipants }; - } catch (error) { + } catch { console.error(error); throw new NotFoundException('No participants', error.toString()); } @@ -4574,7 +4574,7 @@ export class BaileysStartupService extends ChannelStartupService { update.action, ); return { updateParticipants: updateParticipants }; - } catch (error) { + } catch { throw new BadRequestException('Error updating participants', error.toString()); } } @@ -4583,7 +4583,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); return { updateSetting: updateSetting }; - } catch (error) { + } catch { throw new BadRequestException('Error updating setting', error.toString()); } } @@ -4592,7 +4592,7 @@ export class BaileysStartupService extends ChannelStartupService { try { await this.client.groupToggleEphemeral(update.groupJid, update.expiration); return { success: true }; - } catch (error) { + } catch { throw new BadRequestException('Error updating setting', error.toString()); } } @@ -4601,7 +4601,7 @@ export class BaileysStartupService extends ChannelStartupService { try { await this.client.groupLeave(id.groupJid); return { groupJid: id.groupJid, leave: true }; - } catch (error) { + } catch { throw new BadRequestException('Unable to leave the group', error.toString()); } } @@ -4885,7 +4885,7 @@ export class BaileysStartupService extends ChannelStartupService { const response = await this.client.signalRepository.decryptMessage({ jid, type, ciphertext: ciphertextBuffer }); return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response; - } catch (error) { + } catch { this.logger.error('Error decrypting message:'); this.logger.error(error); throw error; @@ -4943,7 +4943,7 @@ export class BaileysStartupService extends ChannelStartupService { catalogLength: productsCatalog.length, catalog: productsCatalog, }; - } catch (error) { + } catch { console.log(error); return { wuid: jid, name: null, isBusiness: false }; } @@ -4964,7 +4964,7 @@ export class BaileysStartupService extends ChannelStartupService { } return catalog; - } catch (error) { + } catch { throw new InternalServerErrorException('Error getCatalog', error.toString()); } } @@ -5008,7 +5008,7 @@ export class BaileysStartupService extends ChannelStartupService { } return result.collections; - } catch (error) { + } catch { throw new InternalServerErrorException('Error getCatalog', error.toString()); } } @@ -5238,7 +5238,9 @@ export class BaileysStartupService extends ChannelStartupService { // Já está descriptografado como nomes de opções selectedOptionNames = selectedOptions; successfulVoterJid = uniqueVoters[0]; - this.logger.verbose(`Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`); + this.logger.verbose( + `Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`, + ); } else { // Está como hash, precisa converter para nomes selectedOptionNames = pollOptions @@ -5272,7 +5274,7 @@ export class BaileysStartupService extends ChannelStartupService { successfulVoterJid = voter; break; } - } catch (error) { + } catch { // Continue tentando outras combinações } } @@ -5293,7 +5295,9 @@ export class BaileysStartupService extends ChannelStartupService { }) .map((option: any) => option.optionName); - this.logger.verbose(`Successfully decrypted vote for voter: ${successfulVoterJid}, creator: ${uniqueCreators[0]}`); + this.logger.verbose( + `Successfully decrypted vote for voter: ${successfulVoterJid}, creator: ${uniqueCreators[0]}`, + ); } else { this.logger.warn(`Failed to decrypt vote. Last error: Could not decrypt with any combination`); continue; @@ -5357,7 +5361,7 @@ export class BaileysStartupService extends ChannelStartupService { results, }, }; - } catch (error) { + } catch { this.logger.error(`Error decrypting poll votes: ${error}`); throw new InternalServerErrorException('Error decrypting poll votes', error.toString()); } diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index aef922cd9..6970fd9b0 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -468,4 +468,4 @@ export const decryptPollVoteSchema: JSONSchema7 = { remoteJid: { type: 'string' }, }, required: ['message', 'remoteJid'], -}; \ No newline at end of file +}; From 2fee5053f37b1ea00bb1a12ddd4dc6ea00fc0353 Mon Sep 17 00:00:00 2001 From: OrionDesign Date: Thu, 11 Dec 2025 17:11:58 -0300 Subject: [PATCH 05/71] =?UTF-8?q?fix(baileys):=20corrigir=20declara=C3=83?= =?UTF-8?q?=C2=A7=C3=83=C2=A3o=20de=20vari=C3=83=C2=A1vel=20error=20em=20b?= =?UTF-8?q?locos=20catch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../whatsapp/whatsapp.baileys.service.ts | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 819138474..cf45a931b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -733,7 +733,7 @@ export class BaileysStartupService extends ChannelStartupService { }); return await this.createClient(number); - } catch { + } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } @@ -742,7 +742,7 @@ export class BaileysStartupService extends ChannelStartupService { public async reloadConnection(): Promise { try { return await this.createClient(this.phoneNumber); - } catch { + } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } @@ -888,7 +888,7 @@ export class BaileysStartupService extends ChannelStartupService { }), ); } - } catch { + } catch (error) { console.error(error); this.logger.error(`Error: ${error.message}`); } @@ -1074,7 +1074,7 @@ export class BaileysStartupService extends ChannelStartupService { contacts = undefined; messages = undefined; chats = undefined; - } catch { + } catch (error) { this.logger.error(error); } }, @@ -1434,7 +1434,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } - } catch { + } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } } @@ -1466,7 +1466,7 @@ export class BaileysStartupService extends ChannelStartupService { messageRaw.message.base64 = buffer.toString('base64'); } } - } catch { + } catch (error) { this.logger.error(['Error converting media to base64', error?.message]); } } @@ -1550,7 +1550,7 @@ export class BaileysStartupService extends ChannelStartupService { create: contactRaw, }); } - } catch { + } catch (error) { this.logger.error(error); } }, @@ -1798,7 +1798,7 @@ export class BaileysStartupService extends ChannelStartupService { }; this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate); - } catch { + } catch (error) { this.logger.error( `Failed to resolve participant data for GROUP_PARTICIPANTS_UPDATE webhook: ${error.message} | Group: ${participantsUpdate.id} | Participants: ${participantsUpdate.participants.length}`, ); @@ -2005,7 +2005,7 @@ export class BaileysStartupService extends ChannelStartupService { return; } } - } catch { + } catch (error) { this.logger.error(error); } }); @@ -2122,7 +2122,7 @@ export class BaileysStartupService extends ChannelStartupService { // return call; return { id: '123', jid, isVideo, callDuration }; - } catch { + } catch (error) { return error; } } @@ -2503,7 +2503,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } - } catch { + } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } } @@ -2534,7 +2534,7 @@ export class BaileysStartupService extends ChannelStartupService { messageRaw.message.base64 = buffer.toString('base64'); } } - } catch { + } catch (error) { this.logger.error(['Error converting media to base64', error?.message]); } } @@ -2555,7 +2555,7 @@ export class BaileysStartupService extends ChannelStartupService { } return messageRaw; - } catch { + } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2607,7 +2607,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { presence: data.presence }; - } catch { + } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2619,7 +2619,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.sendPresenceUpdate(data.presence); return { presence: data.presence }; - } catch { + } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -2860,7 +2860,7 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(`Video duration: ${duration} seconds`); prepareMedia[mediaType].seconds = duration; - } catch { + } catch (error) { this.logger.error('Error getting video duration:'); this.logger.error(error); throw new Error(`Failed to get video duration: ${error.message}`); @@ -2887,7 +2887,7 @@ export class BaileysStartupService extends ChannelStartupService { { [mediaType]: { ...prepareMedia[mediaType] } }, { userJid: this.instance.wuid }, ); - } catch { + } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString() || error); } @@ -2932,7 +2932,7 @@ export class BaileysStartupService extends ChannelStartupService { } else { return await sharp(imageBuffer).webp().toBuffer(); } - } catch { + } catch (error) { console.error('Erro ao converter a imagem para WebP:', error); throw error; } @@ -3683,7 +3683,7 @@ export class BaileysStartupService extends ChannelStartupService { }); await this.client.readMessages(keys); return { message: 'Read messages', read: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Read messages fail', error.toString()); } } @@ -3732,7 +3732,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.chatModify({ archive: data.archive, lastMessages: [last_message] }, createJid(number)); return { chatId: number, archived: true }; - } catch { + } catch (error) { throw new InternalServerErrorException({ archived: false, message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], @@ -3760,7 +3760,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.chatModify({ markRead: false, lastMessages: [last_message] }, createJid(number)); return { chatId: number, markedChatUnread: true }; - } catch { + } catch (error) { throw new InternalServerErrorException({ markedChatUnread: false, message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()], @@ -3817,7 +3817,7 @@ export class BaileysStartupService extends ChannelStartupService { } return response; - } catch { + } catch (error) { throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); } } @@ -3955,7 +3955,7 @@ export class BaileysStartupService extends ChannelStartupService { return result; } - } catch { + } catch (error) { this.logger.error('Error converting audio to mp4:'); this.logger.error(error); throw new BadRequestException('Failed to convert audio to MP4'); @@ -3971,7 +3971,7 @@ export class BaileysStartupService extends ChannelStartupService { base64: buffer.toString('base64'), buffer: getBuffer ? buffer : null, }; - } catch { + } catch (error) { this.logger.error('Error processing media message:'); this.logger.error(error); throw new BadRequestException(error.toString()); @@ -4013,7 +4013,7 @@ export class BaileysStartupService extends ChannelStartupService { groupadd: settings.groupadd, }, }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating privacy settings', error.toString()); } } @@ -4031,7 +4031,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { isBusiness: true, ...profile }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } @@ -4041,7 +4041,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfileName(name); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating profile name', error.toString()); } } @@ -4051,7 +4051,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfileStatus(status); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating profile status', error.toString()); } } @@ -4092,7 +4092,7 @@ export class BaileysStartupService extends ChannelStartupService { this.reloadConnection(); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating profile picture', error.toString()); } } @@ -4104,7 +4104,7 @@ export class BaileysStartupService extends ChannelStartupService { this.reloadConnection(); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error removing profile picture', error.toString()); } } @@ -4124,7 +4124,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateBlockStatus(sender, data.status); return { block: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error blocking user', error.toString()); } } @@ -4150,7 +4150,7 @@ export class BaileysStartupService extends ChannelStartupService { } return null; - } catch { + } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); } @@ -4238,7 +4238,7 @@ export class BaileysStartupService extends ChannelStartupService { } return messageSent; - } catch { + } catch (error) { this.logger.error(error); throw error; } @@ -4278,7 +4278,7 @@ export class BaileysStartupService extends ChannelStartupService { return { numberJid: contact.jid, labelId: data.labelId, remove: true }; } - } catch { + } catch (error) { throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString()); } } @@ -4296,7 +4296,7 @@ export class BaileysStartupService extends ChannelStartupService { } return meta; - } catch { + } catch (error) { this.logger.error(error); return null; } @@ -4344,7 +4344,7 @@ export class BaileysStartupService extends ChannelStartupService { const group = await this.client.groupMetadata(id); return group; - } catch { + } catch (error) { this.logger.error(error); throw new InternalServerErrorException('Error creating group', error.toString()); } @@ -4383,7 +4383,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.updateProfilePicture(picture.groupJid, pic); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error update group picture', error.toString()); } } @@ -4393,7 +4393,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.groupUpdateSubject(data.groupJid, data.subject); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating group subject', error.toString()); } } @@ -4403,7 +4403,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.client.groupUpdateDescription(data.groupJid, data.description); return { update: 'success' }; - } catch { + } catch (error) { throw new InternalServerErrorException('Error updating group description', error.toString()); } } @@ -4437,7 +4437,7 @@ export class BaileysStartupService extends ChannelStartupService { isCommunityAnnounce: group.isCommunityAnnounce, linkedParent: group.linkedParent, }; - } catch { + } catch (error) { if (reply === 'inner') { return; } @@ -4484,7 +4484,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const code = await this.client.groupInviteCode(id.groupJid); return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch { + } catch (error) { throw new NotFoundException('No invite code', error.toString()); } } @@ -4524,7 +4524,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const groupJid = await this.client.groupAcceptInvite(id.inviteCode); return { accepted: true, groupJid: groupJid }; - } catch { + } catch (error) { throw new NotFoundException('Accept invite error', error.toString()); } } @@ -4533,7 +4533,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const inviteCode = await this.client.groupRevokeInvite(id.groupJid); return { revoked: true, inviteCode }; - } catch { + } catch (error) { throw new NotFoundException('Revoke error', error.toString()); } } @@ -4559,7 +4559,7 @@ export class BaileysStartupService extends ChannelStartupService { } return { participants: parsedParticipants }; - } catch { + } catch (error) { console.error(error); throw new NotFoundException('No participants', error.toString()); } @@ -4574,7 +4574,7 @@ export class BaileysStartupService extends ChannelStartupService { update.action, ); return { updateParticipants: updateParticipants }; - } catch { + } catch (error) { throw new BadRequestException('Error updating participants', error.toString()); } } @@ -4583,7 +4583,7 @@ export class BaileysStartupService extends ChannelStartupService { try { const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); return { updateSetting: updateSetting }; - } catch { + } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); } } @@ -4592,7 +4592,7 @@ export class BaileysStartupService extends ChannelStartupService { try { await this.client.groupToggleEphemeral(update.groupJid, update.expiration); return { success: true }; - } catch { + } catch (error) { throw new BadRequestException('Error updating setting', error.toString()); } } @@ -4601,7 +4601,7 @@ export class BaileysStartupService extends ChannelStartupService { try { await this.client.groupLeave(id.groupJid); return { groupJid: id.groupJid, leave: true }; - } catch { + } catch (error) { throw new BadRequestException('Unable to leave the group', error.toString()); } } @@ -4885,7 +4885,7 @@ export class BaileysStartupService extends ChannelStartupService { const response = await this.client.signalRepository.decryptMessage({ jid, type, ciphertext: ciphertextBuffer }); return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response; - } catch { + } catch (error) { this.logger.error('Error decrypting message:'); this.logger.error(error); throw error; @@ -4943,7 +4943,7 @@ export class BaileysStartupService extends ChannelStartupService { catalogLength: productsCatalog.length, catalog: productsCatalog, }; - } catch { + } catch (error) { console.log(error); return { wuid: jid, name: null, isBusiness: false }; } @@ -4964,7 +4964,7 @@ export class BaileysStartupService extends ChannelStartupService { } return catalog; - } catch { + } catch (error) { throw new InternalServerErrorException('Error getCatalog', error.toString()); } } @@ -5008,7 +5008,7 @@ export class BaileysStartupService extends ChannelStartupService { } return result.collections; - } catch { + } catch (error) { throw new InternalServerErrorException('Error getCatalog', error.toString()); } } @@ -5361,7 +5361,7 @@ export class BaileysStartupService extends ChannelStartupService { results, }, }; - } catch { + } catch (error) { this.logger.error(`Error decrypting poll votes: ${error}`); throw new InternalServerErrorException('Error decrypting poll votes', error.toString()); } From 6f2bef678c8ba46f6fc4b9fa84e5ae0a13b08715 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 12 Dec 2025 17:57:44 -0300 Subject: [PATCH 06/71] fix(chat): clean up code formatting by removing unnecessary blank lines in chat controller --- src/api/controllers/chat.controller.ts | 3 ++- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 3 ++- src/api/routes/chat.router.ts | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 27feca83d..fa5ea5fbb 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -121,7 +121,8 @@ export class ChatController { remoteJid: data.remoteJid, }; return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey); - + } + public async fetchChannels({ instanceName }: InstanceDto, query: Query) { return await this.waMonitor.waInstances[instanceName].fetchChannels(query); } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index aad88a374..b75e31a8a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -5382,7 +5382,8 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.error(`Error decrypting poll votes: ${error}`); throw new InternalServerErrorException('Error decrypting poll votes', error.toString()); } - + } + public async fetchChannels(query: Query) { const page = Number((query as any)?.page ?? 1); const limit = Number((query as any)?.limit ?? (query as any)?.rows ?? 50); diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index d90f9b361..28578d9e7 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -290,6 +290,10 @@ export class ChatRouter extends RouterBroker { schema: decryptPollVoteSchema, ClassRef: DecryptPollVoteDto, execute: (instance, data) => chatController.decryptPollVote(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('findChannels'), ...guards, async (req, res) => { const response = await this.dataValidate({ request: req, From 2e3c8184ef4d1b843f8253e5ec0c8527ac7e5f9f Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Mon, 15 Dec 2025 21:38:45 -0300 Subject: [PATCH 07/71] fix(baileys): normalize remote JIDs for consistent database lookups --- .../whatsapp/whatsapp.baileys.service.ts | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index b75e31a8a..8255f5fd3 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1561,7 +1561,12 @@ export class BaileysStartupService extends ChannelStartupService { const readChatToUpdate: Record = {}; // {remoteJid: true} for await (const { key, update } of args) { - if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) { + // Normalize JIDs immediately to ensure consistent DB lookups + const keyAny = key as any; + if (keyAny.remoteJid) keyAny.remoteJid = keyAny.remoteJid.replace(/:.*$/, ''); + if (keyAny.participant) keyAny.participant = keyAny.participant.replace(/:.*$/, ''); + + if (settings?.groupsIgnore && keyAny.remoteJid?.includes('@g.us')) { continue; } @@ -1612,9 +1617,9 @@ export class BaileysStartupService extends ChannelStartupService { const message: any = { keyId: key.id, - remoteJid: key?.remoteJid, + remoteJid: keyAny?.remoteJid?.replace(/:.*$/, ''), fromMe: key.fromMe, - participant: key?.participant, + participant: keyAny?.participant?.replace(/:.*$/, ''), status: status[update.status] ?? 'SERVER_ACK', pollUpdates, instanceId: this.instanceId, @@ -4662,26 +4667,20 @@ export class BaileysStartupService extends ChannelStartupService { return obj; } - private prepareMessage(message: proto.IWebMessageInfo): any { - const contentType = getContentType(message.message); - const contentMsg = message?.message[contentType] as any; - - const messageRaw = { - key: message.key, // Save key exactly as it comes from Baileys - pushName: - message.pushName || - (message.key.fromMe - ? 'Você' - : message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)), - status: status[message.status], - message: this.deserializeMessageBuffers({ ...message.message }), - contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo), - messageType: contentType || 'unknown', - messageTimestamp: Long.isLong(message.messageTimestamp) - ? message.messageTimestamp.toNumber() - : (message.messageTimestamp as number), + private prepareMessage(message: WAMessage): Message { + const keyAny = message.key as any; + const messageRaw: any = { + key: { + ...message.key, + remoteJid: keyAny.remoteJid?.replace(/:.*$/, ''), + participant: keyAny.participant?.replace(/:.*$/, ''), + }, + pushName: message.pushName, + message: message.message, + messageType: getContentType(message.message), + messageTimestamp: message.messageTimestamp, + source: getDevice(keyAny.id), instanceId: this.instanceId, - source: getDevice(message.key.id), }; if (!messageRaw.status && message.key.fromMe === false) { From 72b0833ce26751b9b075752a64423da2077e76be Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Mon, 15 Dec 2025 22:26:52 -0300 Subject: [PATCH 08/71] fix(baileys): cast messageRaw and its properties to any for type safety --- .../whatsapp/whatsapp.baileys.service.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 8255f5fd3..84b9e7851 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1201,10 +1201,10 @@ export class BaileysStartupService extends ChannelStartupService { } } - const messageRaw = this.prepareMessage(received); + const messageRaw = this.prepareMessage(received) as any; if (messageRaw.messageType === 'pollUpdateMessage') { - const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey; + const pollCreationKey = (messageRaw.message as any).pollUpdateMessage.pollCreationMessageKey; const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo; const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any; @@ -1213,7 +1213,7 @@ export class BaileysStartupService extends ChannelStartupService { (pollMessage.message as any).pollCreationMessage?.options || (pollMessage.message as any).pollCreationMessageV3?.options || []; - const pollVote = messageRaw.message.pollUpdateMessage.vote; + const pollVote = (messageRaw.message as any).pollUpdateMessage.vote; const voterJid = received.key.fromMe ? this.instance.wuid @@ -1293,14 +1293,14 @@ export class BaileysStartupService extends ChannelStartupService { }) .map((option) => option.optionName); - messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames; + (messageRaw.message as any).pollUpdateMessage.vote.selectedOptions = selectedOptionNames; const pollUpdates = pollOptions.map((option) => ({ name: option.optionName, voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [], })); - messageRaw.pollUpdates = pollUpdates; + (messageRaw as any).pollUpdates = pollUpdates; } } @@ -1348,13 +1348,14 @@ export class BaileysStartupService extends ChannelStartupService { }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; + (messageRaw.message as any).speechToText = + `[audio] ${await this.openaiService.speechToText(received, this)}`; } } if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { pollUpdates, ...messageData } = messageRaw; + const { pollUpdates, ...messageData } = messageRaw as any; const msg = await this.prismaRepository.message.create({ data: messageData }); const { remoteJid } = received.key; @@ -1430,7 +1431,7 @@ export class BaileysStartupService extends ChannelStartupService { const mediaUrl = await s3Service.getObjectUrl(fullName); - messageRaw.message.mediaUrl = mediaUrl; + (messageRaw.message as any).mediaUrl = mediaUrl; await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } @@ -1452,7 +1453,7 @@ export class BaileysStartupService extends ChannelStartupService { ); if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); + (messageRaw.message as any).base64 = buffer.toString('base64'); } else { // retry to download media const buffer = await downloadMediaMessage( @@ -1463,7 +1464,7 @@ export class BaileysStartupService extends ChannelStartupService { ); if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); + (messageRaw.message as any).base64 = buffer.toString('base64'); } } } catch (error) { @@ -1475,8 +1476,8 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(messageRaw); sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { - messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; + if ((messageRaw.key as any).remoteJid?.includes('@lid') && (messageRaw.key as any).remoteJidAlt) { + (messageRaw.key as any).remoteJid = (messageRaw.key as any).remoteJidAlt; } console.log(messageRaw); @@ -1484,7 +1485,7 @@ export class BaileysStartupService extends ChannelStartupService { await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, - remoteJid: messageRaw.key.remoteJid, + remoteJid: (messageRaw.key as any).remoteJid, msg: messageRaw, pushName: messageRaw.pushName, }); @@ -1513,9 +1514,11 @@ export class BaileysStartupService extends ChannelStartupService { await saveOnWhatsappCache([ { remoteJid: - messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid, - remoteJidAlt: messageRaw.key.remoteJidAlt, - lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null, + (messageRaw.key as any).addressingMode === 'lid' + ? (messageRaw.key as any).remoteJidAlt + : (messageRaw.key as any).remoteJid, + remoteJidAlt: (messageRaw.key as any).remoteJidAlt, + lid: (messageRaw.key as any).addressingMode === 'lid' ? 'lid' : null, }, ]); } @@ -2427,7 +2430,7 @@ export class BaileysStartupService extends ChannelStartupService { messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber(); } - const messageRaw = this.prepareMessage(messageSent); + const messageRaw = this.prepareMessage(messageSent) as any; const isMedia = messageSent?.message?.imageMessage || @@ -2449,14 +2452,15 @@ export class BaileysStartupService extends ChannelStartupService { ); } - if (this.configService.get('OPENAI').ENABLED && messageRaw?.message?.audioMessage) { + if (this.configService.get('OPENAI').ENABLED && (messageRaw as any)?.message?.audioMessage) { const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ where: { instanceId: this.instanceId }, include: { OpenaiCreds: true }, }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`; + (messageRaw.message as any).speechToText = + `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`; } } From f46699ef3f9cdaa3ddcb1f7b6a9f25bf91af5aee Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Mon, 15 Dec 2025 22:35:07 -0300 Subject: [PATCH 09/71] fix(baileys): cast messageRaw and its properties to any for type safety --- .../whatsapp/whatsapp.baileys.service.ts | 25 ++++++++++++------- 1 file changed, 16 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 84b9e7851..33e5eca9c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1564,12 +1564,11 @@ export class BaileysStartupService extends ChannelStartupService { const readChatToUpdate: Record = {}; // {remoteJid: true} for await (const { key, update } of args) { - // Normalize JIDs immediately to ensure consistent DB lookups const keyAny = key as any; - if (keyAny.remoteJid) keyAny.remoteJid = keyAny.remoteJid.replace(/:.*$/, ''); - if (keyAny.participant) keyAny.participant = keyAny.participant.replace(/:.*$/, ''); + const normalizedRemoteJid = keyAny.remoteJid?.replace(/:.*$/, ''); + const normalizedParticipant = keyAny.participant?.replace(/:.*$/, ''); - if (settings?.groupsIgnore && keyAny.remoteJid?.includes('@g.us')) { + if (settings?.groupsIgnore && normalizedRemoteJid?.includes('@g.us')) { continue; } @@ -1620,9 +1619,9 @@ export class BaileysStartupService extends ChannelStartupService { const message: any = { keyId: key.id, - remoteJid: keyAny?.remoteJid?.replace(/:.*$/, ''), + remoteJid: normalizedRemoteJid, fromMe: key.fromMe, - participant: keyAny?.participant?.replace(/:.*$/, ''), + participant: normalizedParticipant, status: status[update.status] ?? 'SERVER_ACK', pollUpdates, instanceId: this.instanceId, @@ -4679,12 +4678,20 @@ export class BaileysStartupService extends ChannelStartupService { remoteJid: keyAny.remoteJid?.replace(/:.*$/, ''), participant: keyAny.participant?.replace(/:.*$/, ''), }, - pushName: message.pushName, - message: message.message, + pushName: + message.pushName || + (message.key.fromMe + ? 'Você' + : message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)), + message: this.deserializeMessageBuffers({ ...message.message }), messageType: getContentType(message.message), - messageTimestamp: message.messageTimestamp, + messageTimestamp: Long.isLong(message.messageTimestamp) + ? message.messageTimestamp.toNumber() + : (message.messageTimestamp as number), source: getDevice(keyAny.id), instanceId: this.instanceId, + status: status[message.status], + contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo), }; if (!messageRaw.status && message.key.fromMe === false) { From 52a8d9ea715505d454d46a837fa4ff439353b52e Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Tue, 16 Dec 2025 11:00:11 -0300 Subject: [PATCH 10/71] fix: normalize remoteJid in message updates and handle race condition in contact cache --- .../whatsapp/whatsapp.baileys.service.ts | 45 +++++++++++++++---- src/utils/onWhatsappCache.ts | 21 +++++++-- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 33e5eca9c..fc9b0b63d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1565,8 +1565,15 @@ export class BaileysStartupService extends ChannelStartupService { for await (const { key, update } of args) { const keyAny = key as any; - const normalizedRemoteJid = keyAny.remoteJid?.replace(/:.*$/, ''); - const normalizedParticipant = keyAny.participant?.replace(/:.*$/, ''); + if (keyAny.remoteJid) { + keyAny.remoteJid = keyAny.remoteJid.replace(/:.*$/, ''); + } + if (keyAny.participant) { + keyAny.participant = keyAny.participant.replace(/:.*$/, ''); + } + + const normalizedRemoteJid = keyAny.remoteJid; + const normalizedParticipant = keyAny.participant; if (settings?.groupsIgnore && normalizedRemoteJid?.includes('@g.us')) { continue; @@ -1644,18 +1651,38 @@ export class BaileysStartupService extends ChannelStartupService { const searchId = originalMessageId || key.id; - const messages = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${searchId} - LIMIT 1 - `) as any[]; - findMessage = messages[0] || null; + let retries = 0; + const maxRetries = 3; + + while (retries < maxRetries) { + const messages = (await this.prismaRepository.$queryRaw` + SELECT * FROM "Message" + WHERE "instanceId" = ${this.instanceId} + AND "key"->>'id' = ${searchId} + LIMIT 1 + `) as any[]; + findMessage = messages[0] || null; + + if (findMessage?.id) { + break; + } + + retries++; + if (retries < maxRetries) { + await delay(2000); + } + } if (!findMessage?.id) { this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); continue; } + if (findMessage?.key?.remoteJid && findMessage.key.remoteJid !== key.remoteJid) { + this.logger.verbose( + `Updating key.remoteJid from ${key.remoteJid} to ${findMessage.key.remoteJid} based on stored message`, + ); + key.remoteJid = findMessage.key.remoteJid; + } message.messageId = findMessage.id; } diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 08de0714e..50fa08c69 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -164,9 +164,24 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { logger.verbose( `[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, ); - await prismaRepository.isOnWhatsapp.create({ - data: dataPayload, - }); + try { + await prismaRepository.isOnWhatsapp.create({ + data: dataPayload, + }); + } catch (error: any) { + // Check for unique constraint violation (Prisma error code P2002) + if (error.code === 'P2002' && error.meta?.target?.includes('remoteJid')) { + logger.verbose( + `[saveOnWhatsappCache] Race condition detected for ${remoteJid}, updating existing record instead.`, + ); + await prismaRepository.isOnWhatsapp.update({ + where: { remoteJid: remoteJid }, + data: dataPayload, + }); + } else { + throw error; + } + } } } catch (e) { // Loga o erro mas não para a execução dos outros promises From cb41e65e29ee549adb23c429f1132c3c559d2ed1 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Tue, 16 Dec 2025 11:32:53 -0300 Subject: [PATCH 11/71] fix: enhance logging for missing original messages during updates --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 +++- 1 file changed, 3 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 fc9b0b63d..cc0acdf87 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1674,7 +1674,9 @@ export class BaileysStartupService extends ChannelStartupService { } if (!findMessage?.id) { - this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); + this.logger.verbose( + `Original message not found for update after ${maxRetries} retries. Skipping. This is expected for protocol messages or ephemeral events not saved to the database. Key: ${JSON.stringify(key)}`, + ); continue; } if (findMessage?.key?.remoteJid && findMessage.key.remoteJid !== key.remoteJid) { From bb831d590f3a605cb87dbdaa625de6ba0b3942c6 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Tue, 16 Dec 2025 12:38:47 -0300 Subject: [PATCH 12/71] refactor: optimize retry loop and robustify cache error handling --- .../channel/whatsapp/whatsapp.baileys.service.ts | 10 +++++++++- src/utils/onWhatsappCache.ts | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index cc0acdf87..52be63394 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1653,6 +1653,7 @@ export class BaileysStartupService extends ChannelStartupService { let retries = 0; const maxRetries = 3; + const retryDelay = 500; // 500ms delay to avoid blocking for too long while (retries < maxRetries) { const messages = (await this.prismaRepository.$queryRaw` @@ -1669,7 +1670,7 @@ export class BaileysStartupService extends ChannelStartupService { retries++; if (retries < maxRetries) { - await delay(2000); + await delay(retryDelay); } } @@ -1679,6 +1680,13 @@ export class BaileysStartupService extends ChannelStartupService { ); continue; } + + // Sync the incoming key.remoteJid with the stored one. + // This mutation is safe and necessary because Baileys events might use LIDs while we store Phone JIDs (or vice versa). + // Normalizing ensuring downstream logic uses the identifier that exists in our database. + if (findMessage?.key?.remoteJid && key.remoteJid !== findMessage.key.remoteJid) { + key.remoteJid = findMessage.key.remoteJid; + } if (findMessage?.key?.remoteJid && findMessage.key.remoteJid !== key.remoteJid) { this.logger.verbose( `Updating key.remoteJid from ${key.remoteJid} to ${findMessage.key.remoteJid} based on stored message`, diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 50fa08c69..8d7a2c16a 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -1,6 +1,7 @@ import { prismaRepository } from '@api/server.module'; import { configService, Database } from '@config/env.config'; import { Logger } from '@config/logger.config'; +import { Prisma } from '@prisma/client'; import dayjs from 'dayjs'; const logger = new Logger('OnWhatsappCache'); @@ -170,7 +171,11 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { }); } catch (error: any) { // Check for unique constraint violation (Prisma error code P2002) - if (error.code === 'P2002' && error.meta?.target?.includes('remoteJid')) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' && + (error.meta?.target as string[])?.includes('remoteJid') + ) { logger.verbose( `[saveOnWhatsappCache] Race condition detected for ${remoteJid}, updating existing record instead.`, ); From 933a28de261f04c80b8cf2e58d9e5c666258dd36 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 16 Dec 2025 14:18:05 -0300 Subject: [PATCH 13/71] feat(baileys): enhance logout process and connection handling - Introduced a flag to prevent reconnection during instance deletion. - Improved logging for connection updates and errors during logout. - Added a delay before reconnection attempts to avoid rapid loops. - Enhanced webhook headers for better tracking and debugging. - Updated configuration to support manual Baileys version setting. --- .../whatsapp/whatsapp.baileys.service.ts | 64 +++++++++++++++++-- .../event/webhook/webhook.controller.ts | 13 +++- src/config/env.config.ts | 5 ++ src/utils/fetchLatestWaWebVersion.ts | 17 +++++ 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index b75e31a8a..3b612d98a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -249,6 +249,7 @@ export class BaileysStartupService extends ChannelStartupService { private readonly msgRetryCounterCache: CacheStore = new NodeCache(); private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false }); private endSession = false; + private isDeleting = false; // Flag to prevent reconnection during deletion private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); @@ -265,10 +266,27 @@ export class BaileysStartupService extends ChannelStartupService { } public async logoutInstance() { + // Mark instance as deleting to prevent reconnection attempts + this.isDeleting = true; + this.endSession = true; + this.messageProcessor.onDestroy(); - await this.client?.logout('Log out instance: ' + this.instanceName); - this.client?.ws?.close(); + if (this.client) { + try { + await this.client.logout('Log out instance: ' + this.instanceName); + } catch (error) { + this.logger.error({ message: 'Error during logout', error }); + } + + // Improved socket cleanup + try { + this.client.ws?.close(); + this.client.end(new Error('Instance logout')); + } catch (error) { + this.logger.error({ message: 'Error during socket cleanup', error }); + } + } const db = this.configService.get('DATABASE'); const cache = this.configService.get('CACHE'); @@ -332,6 +350,18 @@ export class BaileysStartupService extends ChannelStartupService { } private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + // Enhanced logging for connection updates + const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; + this.logger.info({ + message: 'Connection update received', + connection, + hasQr: !!qr, + statusCode, + instanceName: this.instance.name, + isDeleting: this.isDeleting, + endSession: this.endSession, + }); + if (qr) { if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { this.sendDataWebhook(Events.QRCODE_UPDATED, { @@ -424,11 +454,29 @@ export class BaileysStartupService extends ChannelStartupService { } if (connection === 'close') { + // Check if instance is being deleted or session is ending + if (this.isDeleting || this.endSession) { + this.logger.info('Instance is being deleted/ended, skipping reconnection attempt'); + return; + } + const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; const shouldReconnect = !codesToNotReconnect.includes(statusCode); + + this.logger.info({ + message: 'Connection closed, evaluating reconnection', + statusCode, + shouldReconnect, + instanceName: this.instance.name, + }); + if (shouldReconnect) { - await this.connectToWhatsapp(this.phoneNumber); + // Add 3 second delay before reconnection to prevent rapid reconnection loops + this.logger.info('Reconnecting in 3 seconds...'); + setTimeout(async () => { + await this.connectToWhatsapp(this.phoneNumber); + }, 3000); } else { this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, @@ -591,10 +639,11 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.info(`Browser: ${browser}`); } + // Fetch latest WhatsApp Web version automatically const baileysVersion = await fetchLatestWaWebVersion({}); const version = baileysVersion.version; - const log = `Baileys version: ${version.join('.')}`; + const log = `Baileys version: ${version.join('.')}`; this.logger.info(log); this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`); @@ -602,7 +651,7 @@ export class BaileysStartupService extends ChannelStartupService { let options; if (this.localProxy?.enabled) { - this.logger.info('Proxy enabled: ' + this.localProxy?.host); + this.logger.verbose('Proxy enabled'); if (this.localProxy?.host?.includes('proxyscrape')) { try { @@ -611,9 +660,10 @@ export class BaileysStartupService extends ChannelStartupService { const proxyUrls = text.split('\r\n'); const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); const proxyUrl = 'http://' + proxyUrls[rand]; + this.logger.info('Proxy url: ' + proxyUrl); options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) }; - } catch { - this.localProxy.enabled = false; + } catch (error) { + this.logger.error(error); } } else { options = { diff --git a/src/api/integrations/event/webhook/webhook.controller.ts b/src/api/integrations/event/webhook/webhook.controller.ts index 7f1dd8dc0..5357554f9 100644 --- a/src/api/integrations/event/webhook/webhook.controller.ts +++ b/src/api/integrations/event/webhook/webhook.controller.ts @@ -124,9 +124,20 @@ export class WebhookController extends EventController implements EventControlle try { if (instance?.enabled && regex.test(instance.url)) { + // Add custom headers for better webhook tracking and debugging + const enhancedHeaders = { + ...webhookHeaders, + 'Content-Type': 'application/json', + 'X-Instance-ID': this.monitor.waInstances[instanceName].instanceId, + 'X-Instance-Name': instanceName, + 'X-Event-Type': event, + 'X-Timestamp': Date.now().toString(), + 'User-Agent': 'EvolutionAPI-Webhook/2.3.7', + }; + const httpService = axios.create({ baseURL, - headers: webhookHeaders as Record | undefined, + headers: enhancedHeaders as Record, timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, }); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7c4e382e7..199ffb380 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -313,6 +313,7 @@ export type Webhook = { }; export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher }; export type ConfigSessionPhone = { CLIENT: string; NAME: string }; +export type Baileys = { VERSION?: string }; export type QrCode = { LIMIT: number; COLOR: string }; export type Typebot = { ENABLED: boolean; API_VERSION: string; SEND_MEDIA_BASE64: boolean }; export type Chatwoot = { @@ -410,6 +411,7 @@ export interface Env { WEBHOOK: Webhook; PUSHER: Pusher; CONFIG_SESSION_PHONE: ConfigSessionPhone; + BAILEYS: Baileys; QRCODE: QrCode; TYPEBOT: Typebot; CHATWOOT: Chatwoot; @@ -800,6 +802,9 @@ export class ConfigService { CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome', }, + BAILEYS: { + VERSION: process.env?.CONFIG_BAILEYS_VERSION, + }, QRCODE: { LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, COLOR: process.env.QRCODE_COLOR || '#198754', diff --git a/src/utils/fetchLatestWaWebVersion.ts b/src/utils/fetchLatestWaWebVersion.ts index 6dcfb797e..f6b0aa6d6 100644 --- a/src/utils/fetchLatestWaWebVersion.ts +++ b/src/utils/fetchLatestWaWebVersion.ts @@ -1,7 +1,24 @@ import axios, { AxiosRequestConfig } from 'axios'; import { fetchLatestBaileysVersion, WAVersion } from 'baileys'; +import { Baileys, configService } from '../config/env.config'; + export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => { + // Check if manual version is set via configuration + const baileysConfig = configService.get('BAILEYS'); + const manualVersion = baileysConfig?.VERSION; + + if (manualVersion) { + const versionParts = manualVersion.split('.').map(Number); + if (versionParts.length === 3 && !versionParts.some(isNaN)) { + return { + version: versionParts as WAVersion, + isLatest: false, + isManual: true, + }; + } + } + try { const { data } = await axios.get('https://web.whatsapp.com/sw.js', { ...options, From 6efa87908159ba8eebca9d25d0fa6495dfe5cb5f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 16 Dec 2025 14:32:26 -0300 Subject: [PATCH 14/71] chore: increase token length in Instance model across MySQL, PostgreSQL, and PSQL Bouncer schemas --- .../20251216143054_increase_token_length/migration.sql | 3 +++ prisma/mysql-schema.prisma | 2 +- .../20251216143054_increase_token_length/migration.sql | 3 +++ prisma/postgresql-schema.prisma | 2 +- prisma/psql_bouncer-schema.prisma | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 prisma/mysql-migrations/20251216143054_increase_token_length/migration.sql create mode 100644 prisma/postgresql-migrations/20251216143054_increase_token_length/migration.sql diff --git a/prisma/mysql-migrations/20251216143054_increase_token_length/migration.sql b/prisma/mysql-migrations/20251216143054_increase_token_length/migration.sql new file mode 100644 index 000000000..7c7d06dce --- /dev/null +++ b/prisma/mysql-migrations/20251216143054_increase_token_length/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE `Instance` MODIFY `token` VARCHAR(500); + diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 71b5a743f..58a796481 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -70,7 +70,7 @@ model Instance { integration String? @db.VarChar(100) number String? @db.VarChar(100) businessId String? @db.VarChar(100) - token String? @db.VarChar(255) + token String? @db.VarChar(500) clientName String? @db.VarChar(100) disconnectionReasonCode Int? @db.Int disconnectionObject Json? @db.Json diff --git a/prisma/postgresql-migrations/20251216143054_increase_token_length/migration.sql b/prisma/postgresql-migrations/20251216143054_increase_token_length/migration.sql new file mode 100644 index 000000000..dc6c699de --- /dev/null +++ b/prisma/postgresql-migrations/20251216143054_increase_token_length/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Instance" ALTER COLUMN "token" TYPE VARCHAR(500); + diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 6b98f88da..51b3c97c8 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -70,7 +70,7 @@ model Instance { integration String? @db.VarChar(100) number String? @db.VarChar(100) businessId String? @db.VarChar(100) - token String? @db.VarChar(255) + token String? @db.VarChar(500) clientName String? @db.VarChar(100) disconnectionReasonCode Int? @db.Integer disconnectionObject Json? @db.JsonB diff --git a/prisma/psql_bouncer-schema.prisma b/prisma/psql_bouncer-schema.prisma index a3f4dbe90..52e9eb784 100644 --- a/prisma/psql_bouncer-schema.prisma +++ b/prisma/psql_bouncer-schema.prisma @@ -71,7 +71,7 @@ model Instance { integration String? @db.VarChar(100) number String? @db.VarChar(100) businessId String? @db.VarChar(100) - token String? @db.VarChar(255) + token String? @db.VarChar(500) clientName String? @db.VarChar(100) disconnectionReasonCode Int? @db.Integer disconnectionObject Json? @db.JsonB From d6262ca4f4f41200c53cd7b1aad2ba53b5c83067 Mon Sep 17 00:00:00 2001 From: augustolima1 <62573696+augustolima1@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:47:00 -0300 Subject: [PATCH 15/71] fix(mysql): compatibilidade da coluna lid e queries RAW --- .../migration.sql | 3 +- prisma/mysql-schema.prisma | 1 + .../whatsapp/whatsapp.baileys.service.ts | 240 +++++++++++++----- .../chatwoot/services/chatwoot.service.ts | 73 ++++-- src/api/services/channel.service.ts | 170 +++++++++---- 5 files changed, 343 insertions(+), 144 deletions(-) diff --git a/prisma/mysql-migrations/20250918183910_add_kafka_integration/migration.sql b/prisma/mysql-migrations/20250918183910_add_kafka_integration/migration.sql index c3a089bd0..7db06e1ae 100644 --- a/prisma/mysql-migrations/20250918183910_add_kafka_integration/migration.sql +++ b/prisma/mysql-migrations/20250918183910_add_kafka_integration/migration.sql @@ -131,8 +131,7 @@ ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRE MODIFY `updatedAt` TIMESTAMP NOT NULL; -- AlterTable -ALTER TABLE `IsOnWhatsapp` DROP COLUMN `lid`, - MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, MODIFY `updatedAt` TIMESTAMP NOT NULL; -- AlterTable diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 71b5a743f..63ef9377d 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -655,6 +655,7 @@ model IsOnWhatsapp { id String @id @default(cuid()) remoteJid String @unique @db.VarChar(100) jidOptions String + lid String? @db.VarChar(100) createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..089d4ca7b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -522,12 +522,27 @@ export class BaileysStartupService extends ChannelStartupService { private async getMessage(key: proto.IMessageKey, full = false) { try { - // Use raw SQL to avoid JSON path issues - const webMessageInfo = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${key.id} - `) as proto.IWebMessageInfo[]; + const provider = this.configService.get('DATABASE').PROVIDER; + + let webMessageInfo: proto.IWebMessageInfo[]; + + if (provider === 'mysql') { + // MySQL version + webMessageInfo = (await this.prismaRepository.$queryRaw` + SELECT * FROM Message + WHERE instanceId = ${this.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${key.id} + LIMIT 1 + `) as proto.IWebMessageInfo[]; + } else { + // PostgreSQL version + webMessageInfo = (await this.prismaRepository.$queryRaw` + SELECT * FROM "Message" + WHERE "instanceId" = ${this.instanceId} + AND "key"->>'id' = ${key.id} + LIMIT 1 + `) as proto.IWebMessageInfo[]; + } if (full) { return webMessageInfo[0]; @@ -1636,13 +1651,24 @@ export class BaileysStartupService extends ChannelStartupService { } const searchId = originalMessageId || key.id; - - const messages = (await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'id' = ${searchId} - LIMIT 1 - `) as any[]; + const dbProvider = this.configService.get('DATABASE').PROVIDER; + + let messages: any[]; + if (dbProvider === 'mysql') { + messages = (await this.prismaRepository.$queryRaw` + SELECT * FROM Message + WHERE instanceId = ${this.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${searchId} + LIMIT 1 + `) as any[]; + } else { + messages = (await this.prismaRepository.$queryRaw` + SELECT * FROM "Message" + WHERE "instanceId" = ${this.instanceId} + AND "key"->>'id' = ${searchId} + LIMIT 1 + `) as any[]; + } findMessage = messages[0] || null; if (!findMessage?.id) { @@ -4734,16 +4760,32 @@ export class BaileysStartupService extends ChannelStartupService { private async updateMessagesReadedByTimestamp(remoteJid: string, timestamp?: number): Promise { if (timestamp === undefined || timestamp === null) return 0; - // Use raw SQL to avoid JSON path issues - const result = await this.prismaRepository.$executeRaw` - UPDATE "Message" - SET "status" = ${status[4]} - WHERE "instanceId" = ${this.instanceId} - AND "key"->>'remoteJid' = ${remoteJid} - AND ("key"->>'fromMe')::boolean = false - AND "messageTimestamp" <= ${timestamp} - AND ("status" IS NULL OR "status" = ${status[3]}) - `; + const provider = this.configService.get('DATABASE').PROVIDER; + let result: number; + + if (provider === 'mysql') { + // MySQL version + result = await this.prismaRepository.$executeRaw` + UPDATE Message + SET status = ${status[4]} + WHERE instanceId = ${this.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.remoteJid')) = ${remoteJid} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.fromMe')) = 'false' + AND messageTimestamp <= ${timestamp} + AND (status IS NULL OR status = ${status[3]}) + `; + } else { + // PostgreSQL version + result = await this.prismaRepository.$executeRaw` + UPDATE "Message" + SET "status" = ${status[4]} + WHERE "instanceId" = ${this.instanceId} + AND "key"->>'remoteJid' = ${remoteJid} + AND ("key"->>'fromMe')::boolean = false + AND "messageTimestamp" <= ${timestamp} + AND ("status" IS NULL OR "status" = ${status[3]}) + `; + } if (result) { if (result > 0) { @@ -4757,16 +4799,33 @@ export class BaileysStartupService extends ChannelStartupService { } private async updateChatUnreadMessages(remoteJid: string): Promise { - const [chat, unreadMessages] = await Promise.all([ - this.prismaRepository.chat.findFirst({ where: { remoteJid } }), - // Use raw SQL to avoid JSON path issues - this.prismaRepository.$queryRaw` + const provider = this.configService.get('DATABASE').PROVIDER; + + let unreadMessagesPromise: Promise; + + if (provider === 'mysql') { + // MySQL version + unreadMessagesPromise = this.prismaRepository.$queryRaw` + SELECT COUNT(*) as count FROM Message + WHERE instanceId = ${this.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.remoteJid')) = ${remoteJid} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.fromMe')) = 'false' + AND status = ${status[3]} + `.then((result: any[]) => Number(result[0]?.count) || 0); + } else { + // PostgreSQL version + unreadMessagesPromise = this.prismaRepository.$queryRaw` SELECT COUNT(*)::int as count FROM "Message" WHERE "instanceId" = ${this.instanceId} AND "key"->>'remoteJid' = ${remoteJid} AND ("key"->>'fromMe')::boolean = false AND "status" = ${status[3]} - `.then((result: any[]) => result[0]?.count || 0), + `.then((result: any[]) => result[0]?.count || 0); + } + + const [chat, unreadMessages] = await Promise.all([ + this.prismaRepository.chat.findFirst({ where: { remoteJid } }), + unreadMessagesPromise, ]); if (chat && chat.unreadMessages !== unreadMessages) { @@ -4778,50 +4837,95 @@ export class BaileysStartupService extends ChannelStartupService { private async addLabel(labelId: string, instanceId: string, chatId: string) { const id = cuid(); - - await this.prismaRepository.$executeRawUnsafe( - `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") - VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") - DO - UPDATE - SET "labels" = ( - SELECT to_jsonb(array_agg(DISTINCT elem)) - FROM ( - SELECT jsonb_array_elements_text("Chat"."labels") AS elem - UNION - SELECT $1::text AS elem - ) sub - ), - "updatedAt" = NOW();`, - labelId, - instanceId, - chatId, - id, - ); + const provider = this.configService.get('DATABASE').PROVIDER; + + if (provider === 'mysql') { + // MySQL version - use INSERT ... ON DUPLICATE KEY UPDATE + await this.prismaRepository.$executeRawUnsafe( + `INSERT INTO Chat (id, instanceId, remoteJid, labels, createdAt, updatedAt) + VALUES (?, ?, ?, JSON_ARRAY(?), NOW(), NOW()) + ON DUPLICATE KEY UPDATE + labels = JSON_ARRAY_APPEND( + COALESCE(labels, JSON_ARRAY()), + '$', + ? + ), + updatedAt = NOW()`, + id, + instanceId, + chatId, + labelId, + labelId, + ); + } else { + // PostgreSQL version + await this.prismaRepository.$executeRawUnsafe( + `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") + VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") + DO + UPDATE + SET "labels" = ( + SELECT to_jsonb(array_agg(DISTINCT elem)) + FROM ( + SELECT jsonb_array_elements_text("Chat"."labels") AS elem + UNION + SELECT $1::text AS elem + ) sub + ), + "updatedAt" = NOW();`, + labelId, + instanceId, + chatId, + id, + ); + } } private async removeLabel(labelId: string, instanceId: string, chatId: string) { const id = cuid(); - - await this.prismaRepository.$executeRawUnsafe( - `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") - VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") - DO - UPDATE - SET "labels" = COALESCE ( - ( - SELECT jsonb_agg(elem) - FROM jsonb_array_elements_text("Chat"."labels") AS elem - WHERE elem <> $1 - ), - '[]'::jsonb - ), - "updatedAt" = NOW();`, - labelId, - instanceId, - chatId, - id, - ); + const provider = this.configService.get('DATABASE').PROVIDER; + + if (provider === 'mysql') { + // MySQL version - use INSERT ... ON DUPLICATE KEY UPDATE + await this.prismaRepository.$executeRawUnsafe( + `INSERT INTO Chat (id, instanceId, remoteJid, labels, createdAt, updatedAt) + VALUES (?, ?, ?, JSON_ARRAY(), NOW(), NOW()) + ON DUPLICATE KEY UPDATE + labels = COALESCE( + JSON_REMOVE( + labels, + JSON_UNQUOTE(JSON_SEARCH(labels, 'one', ?)) + ), + JSON_ARRAY() + ), + updatedAt = NOW()`, + id, + instanceId, + chatId, + labelId, + ); + } else { + // PostgreSQL version + await this.prismaRepository.$executeRawUnsafe( + `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") + VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") + DO + UPDATE + SET "labels" = COALESCE ( + ( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements_text("Chat"."labels") AS elem + WHERE elem <> $1 + ), + '[]'::jsonb + ), + "updatedAt" = NOW();`, + labelId, + instanceId, + chatId, + id, + ); + } } public async baileysOnWhatsapp(jid: string) { diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff188..d15b19804 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1617,18 +1617,36 @@ export class ChatwootService { return; } - // Use raw SQL to avoid JSON path issues - const result = await this.prismaRepository.$executeRaw` - UPDATE "Message" - SET - "chatwootMessageId" = ${chatwootMessageIds.messageId}, - "chatwootConversationId" = ${chatwootMessageIds.conversationId}, - "chatwootInboxId" = ${chatwootMessageIds.inboxId}, - "chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId}, - "chatwootIsRead" = ${chatwootMessageIds.isRead || false} - WHERE "instanceId" = ${instance.instanceId} - AND "key"->>'id' = ${key.id} - `; + const provider = this.configService.get('DATABASE').PROVIDER; + let result: number; + + if (provider === 'mysql') { + // MySQL version + result = await this.prismaRepository.$executeRaw` + UPDATE Message + SET + chatwootMessageId = ${chatwootMessageIds.messageId}, + chatwootConversationId = ${chatwootMessageIds.conversationId}, + chatwootInboxId = ${chatwootMessageIds.inboxId}, + chatwootContactInboxSourceId = ${chatwootMessageIds.contactInboxSourceId}, + chatwootIsRead = ${chatwootMessageIds.isRead || false} + WHERE instanceId = ${instance.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${key.id} + `; + } else { + // PostgreSQL version + result = await this.prismaRepository.$executeRaw` + UPDATE "Message" + SET + "chatwootMessageId" = ${chatwootMessageIds.messageId}, + "chatwootConversationId" = ${chatwootMessageIds.conversationId}, + "chatwootInboxId" = ${chatwootMessageIds.inboxId}, + "chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId}, + "chatwootIsRead" = ${chatwootMessageIds.isRead || false} + WHERE "instanceId" = ${instance.instanceId} + AND "key"->>'id' = ${key.id} + `; + } this.logger.verbose(`Update result: ${result} rows affected`); @@ -1642,15 +1660,28 @@ export class ChatwootService { } private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { - // Use raw SQL query to avoid JSON path issues with Prisma - const messages = await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${instance.instanceId} - AND "key"->>'id' = ${keyId} - LIMIT 1 - `; - - return (messages as MessageModel[])[0] || null; + const provider = this.configService.get('DATABASE').PROVIDER; + let messages: MessageModel[]; + + if (provider === 'mysql') { + // MySQL version + messages = await this.prismaRepository.$queryRaw` + SELECT * FROM Message + WHERE instanceId = ${instance.instanceId} + AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${keyId} + LIMIT 1 + `; + } else { + // PostgreSQL version + messages = await this.prismaRepository.$queryRaw` + SELECT * FROM "Message" + WHERE "instanceId" = ${instance.instanceId} + AND "key"->>'id' = ${keyId} + LIMIT 1 + `; + } + + return messages[0] || null; } private async getReplyToIds( diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 56bec0802..2d7ec8c60 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -9,7 +9,7 @@ import { TypebotService } from '@api/integrations/chatbot/typebot/services/typeb import { PrismaRepository, Query } from '@api/repository/repository.service'; import { eventManager, waMonitor } from '@api/server.module'; import { Events, wa } from '@api/types/wa.types'; -import { Auth, Chatwoot, ConfigService, HttpServer, Proxy } from '@config/env.config'; +import { Auth, Chatwoot, ConfigService, Database, HttpServer, Proxy } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { NotFoundException } from '@exceptions'; import { Contact, Message, Prisma } from '@prisma/client'; @@ -731,63 +731,127 @@ export class ChannelStartupService { where['remoteJid'] = remoteJid; } - const timestampFilter = - query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte - ? Prisma.sql` - AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)} - AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}` - : Prisma.sql``; - + const provider = this.configService.get('DATABASE').PROVIDER; const limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``; const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : Prisma.sql``; - const results = await this.prismaRepository.$queryRaw` - WITH rankedMessages AS ( - SELECT DISTINCT ON ("Message"."key"->>'remoteJid') - "Contact"."id" as "contactId", - "Message"."key"->>'remoteJid' as "remoteJid", - CASE - WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName") - ELSE COALESCE("Contact"."pushName", "Message"."pushName") - END as "pushName", - "Contact"."profilePicUrl", + let results: any[]; + + if (provider === 'mysql') { + // MySQL version + const timestampFilterMysql = + query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte + ? Prisma.sql` + AND Message.messageTimestamp >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)} + AND Message.messageTimestamp <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}` + : Prisma.sql``; + + results = await this.prismaRepository.$queryRaw` + SELECT + Contact.id as contactId, + JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) as remoteJid, + CASE + WHEN JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) LIKE '%@g.us' THEN COALESCE(Chat.name, Contact.pushName) + ELSE COALESCE(Contact.pushName, Message.pushName) + END as pushName, + Contact.profilePicUrl, COALESCE( - to_timestamp("Message"."messageTimestamp"::double precision), - "Contact"."updatedAt" - ) as "updatedAt", - "Chat"."name" as "pushName", - "Chat"."createdAt" as "windowStart", - "Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires", - "Chat"."unreadMessages" as "unreadMessages", - CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive", - "Message"."id" AS "lastMessageId", - "Message"."key" AS "lastMessage_key", + FROM_UNIXTIME(Message.messageTimestamp), + Contact.updatedAt + ) as updatedAt, + Chat.name as chatName, + Chat.createdAt as windowStart, + DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires, + Chat.unreadMessages as unreadMessages, + CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive, + Message.id AS lastMessageId, + Message.key AS lastMessage_key, CASE - WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você' - ELSE "Message"."pushName" - END AS "lastMessagePushName", - "Message"."participant" AS "lastMessageParticipant", - "Message"."messageType" AS "lastMessageMessageType", - "Message"."message" AS "lastMessageMessage", - "Message"."contextInfo" AS "lastMessageContextInfo", - "Message"."source" AS "lastMessageSource", - "Message"."messageTimestamp" AS "lastMessageMessageTimestamp", - "Message"."instanceId" AS "lastMessageInstanceId", - "Message"."sessionId" AS "lastMessageSessionId", - "Message"."status" AS "lastMessageStatus" - FROM "Message" - LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId" - LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId" - WHERE "Message"."instanceId" = ${this.instanceId} - ${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``} - ${timestampFilter} - ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC - ) - SELECT * FROM rankedMessages - ORDER BY "updatedAt" DESC NULLS LAST - ${limit} - ${offset}; - `; + WHEN JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.fromMe')) = 'true' THEN 'Você' + ELSE Message.pushName + END AS lastMessagePushName, + Message.participant AS lastMessageParticipant, + Message.messageType AS lastMessageMessageType, + Message.message AS lastMessageMessage, + Message.contextInfo AS lastMessageContextInfo, + Message.source AS lastMessageSource, + Message.messageTimestamp AS lastMessageMessageTimestamp, + Message.instanceId AS lastMessageInstanceId, + Message.sessionId AS lastMessageSessionId, + Message.status AS lastMessageStatus + FROM Message + LEFT JOIN Contact ON Contact.remoteJid = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) AND Contact.instanceId = Message.instanceId + LEFT JOIN Chat ON Chat.remoteJid = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) AND Chat.instanceId = Message.instanceId + WHERE Message.instanceId = ${this.instanceId} + ${remoteJid ? Prisma.sql`AND JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) = ${remoteJid}` : Prisma.sql``} + ${timestampFilterMysql} + AND Message.messageTimestamp = ( + SELECT MAX(m2.messageTimestamp) + FROM Message m2 + WHERE JSON_UNQUOTE(JSON_EXTRACT(m2.key, '$.remoteJid')) = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) + AND m2.instanceId = Message.instanceId + ) + ORDER BY updatedAt DESC + ${limit} + ${offset}; + `; + } else { + // PostgreSQL version + const timestampFilter = + query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte + ? Prisma.sql` + AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)} + AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}` + : Prisma.sql``; + + results = await this.prismaRepository.$queryRaw` + WITH rankedMessages AS ( + SELECT DISTINCT ON ("Message"."key"->>'remoteJid') + "Contact"."id" as "contactId", + "Message"."key"->>'remoteJid' as "remoteJid", + CASE + WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName") + ELSE COALESCE("Contact"."pushName", "Message"."pushName") + END as "pushName", + "Contact"."profilePicUrl", + COALESCE( + to_timestamp("Message"."messageTimestamp"::double precision), + "Contact"."updatedAt" + ) as "updatedAt", + "Chat"."name" as "pushName", + "Chat"."createdAt" as "windowStart", + "Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires", + "Chat"."unreadMessages" as "unreadMessages", + CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive", + "Message"."id" AS "lastMessageId", + "Message"."key" AS "lastMessage_key", + CASE + WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você' + ELSE "Message"."pushName" + END AS "lastMessagePushName", + "Message"."participant" AS "lastMessageParticipant", + "Message"."messageType" AS "lastMessageMessageType", + "Message"."message" AS "lastMessageMessage", + "Message"."contextInfo" AS "lastMessageContextInfo", + "Message"."source" AS "lastMessageSource", + "Message"."messageTimestamp" AS "lastMessageMessageTimestamp", + "Message"."instanceId" AS "lastMessageInstanceId", + "Message"."sessionId" AS "lastMessageSessionId", + "Message"."status" AS "lastMessageStatus" + FROM "Message" + LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId" + LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId" + WHERE "Message"."instanceId" = ${this.instanceId} + ${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``} + ${timestampFilter} + ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC + ) + SELECT * FROM rankedMessages + ORDER BY "updatedAt" DESC NULLS LAST + ${limit} + ${offset}; + `; + } if (results && isArray(results) && results.length > 0) { const mappedResults = results.map((contact) => { From 0aedee53d4940cb6b7d48066b19d64c979042bd7 Mon Sep 17 00:00:00 2001 From: augustolima1 <62573696+augustolima1@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:55:22 -0300 Subject: [PATCH 16/71] fix: add migration to re-add lid column for existing installations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates new migration to ensure lid column exists even in databases where it was previously dropped by the Kafka integration migration. Uses prepared statement to check column existence before adding, ensuring compatibility with both fresh and existing installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../migration.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 prisma/mysql-migrations/20251223093839_re_add_lid_to_is_onwhatsapp/migration.sql diff --git a/prisma/mysql-migrations/20251223093839_re_add_lid_to_is_onwhatsapp/migration.sql b/prisma/mysql-migrations/20251223093839_re_add_lid_to_is_onwhatsapp/migration.sql new file mode 100644 index 000000000..1a6046d67 --- /dev/null +++ b/prisma/mysql-migrations/20251223093839_re_add_lid_to_is_onwhatsapp/migration.sql @@ -0,0 +1,21 @@ +-- Re-add lid column that was incorrectly dropped by previous migration +-- This migration ensures backward compatibility for existing installations + +-- Check if column exists before adding +SET @dbname = DATABASE(); +SET @tablename = 'IsOnWhatsapp'; +SET @columnname = 'lid'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + 'SELECT 1', + CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(100);') +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; From cf8f0b3e120db7a4416e91fa85aabe26a38e41d3 Mon Sep 17 00:00:00 2001 From: Fernando Figueroa Date: Thu, 1 Jan 2026 16:32:17 -0300 Subject: [PATCH 17/71] feat(audio): add waveform visualization for PTT voice messages - Add audio-decode library for audio buffer analysis - Implement getAudioDuration() to extract duration from audio - Implement getAudioWaveform() to generate 64-value waveform array - Normalize waveform values to 0-100 range for WhatsApp compatibility - Change audio bitrate from 128k to 48k per WhatsApp PTT requirements - Add Baileys patch to prevent waveform overwrite - Increase Node.js heap size for build to prevent OOM Fixes #1086 --- Dockerfile | 5 +- package-lock.json | 261 ++++++++++++++++++ package.json | 2 + patches/baileys+7.0.0-rc.6.patch | 13 + .../whatsapp/whatsapp.baileys.service.ts | 72 ++++- 5 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 patches/baileys+7.0.0-rc.6.patch diff --git a/Dockerfile b/Dockerfile index 24c4e3bc7..747e7d730 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,12 @@ WORKDIR /evolution COPY ./package*.json ./ COPY ./tsconfig.json ./ COPY ./tsup.config.ts ./ +COPY ./patches ./patches RUN npm ci --silent +RUN npx patch-package + COPY ./src ./src COPY ./public ./public COPY ./prisma ./prisma @@ -28,7 +31,7 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* RUN ./Docker/scripts/generate_database.sh -RUN npm run build +RUN NODE_OPTIONS="--max-old-space-size=2048" npm run build FROM node:24-alpine AS final diff --git a/package-lock.json b/package-lock.json index c9c50513a..476a88c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", @@ -5407,6 +5408,13 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -6338,6 +6346,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -8780,6 +8804,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/findup-sync": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", @@ -10054,6 +10088,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10397,6 +10447,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10534,6 +10597,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10567,6 +10650,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -10664,6 +10757,16 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -12216,6 +12319,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -12624,6 +12744,137 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -14307,6 +14558,16 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", diff --git a/package.json b/package.json index 009782b7c..ce83b476f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", + "postinstall": "patch-package", "prepare": "husky" }, "repository": { @@ -147,6 +148,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", diff --git a/patches/baileys+7.0.0-rc.6.patch b/patches/baileys+7.0.0-rc.6.patch new file mode 100644 index 000000000..e01f5cac3 --- /dev/null +++ b/patches/baileys+7.0.0-rc.6.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/baileys/lib/Utils/messages.js b/node_modules/baileys/lib/Utils/messages.js +index 17b05b8..782efb4 100644 +--- a/node_modules/baileys/lib/Utils/messages.js ++++ b/node_modules/baileys/lib/Utils/messages.js +@@ -132,7 +132,7 @@ export const prepareWAMessageMedia = async (message, options) => { + } + const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'; + const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'; +- const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true; ++ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined'; + const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true; + const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation; + const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, { diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 1e3bdcf13..0ed4ec2da 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -90,6 +90,7 @@ import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import axios from 'axios'; +import audioDecode from 'audio-decode'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -3006,7 +3007,7 @@ export class BaileysStartupService extends ChannelStartupService { .noVideo() .audioCodec('libopus') .addOutputOptions('-avoid_negative_ts make_zero') - .audioBitrate('128k') + .audioBitrate('48k') .audioFrequency(48000) .audioChannels(1) .outputOptions([ @@ -3038,6 +3039,58 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async getAudioMetadata(audioBuffer: Buffer): Promise<{ seconds: number; waveform: Uint8Array }> { + try { + this.logger.debug('Decoding audio buffer for metadata extraction...'); + const audioData = await audioDecode(audioBuffer); + + // Extract duration + const seconds = Math.ceil(audioData.duration); + this.logger.debug(`Audio duration: ${seconds} seconds`); + + // Generate waveform + const samples = audioData.getChannelData(0); + const waveformLength = 64; + const samplesPerWaveform = Math.max(1, Math.floor(samples.length / waveformLength)); + + // First pass: calculate raw averages + const rawValues: number[] = []; + for (let i = 0; i < waveformLength; i++) { + const start = i * samplesPerWaveform; + const end = start + samplesPerWaveform; + let sum = 0; + for (let j = start; j < end && j < samples.length; j++) { + sum += Math.abs(samples[j]); + } + const avg = sum / samplesPerWaveform; + rawValues.push(avg); + } + + // Find max value for normalization + const maxValue = Math.max(...rawValues); + + // Second pass: normalize to 0-100 range + const waveform = new Uint8Array(waveformLength); + if (maxValue > 0) { + for (let i = 0; i < waveformLength; i++) { + const normalized = Math.floor((rawValues[i] / maxValue) * 100); + waveform[i] = rawValues[i] > 0 ? Math.max(5, Math.min(100, normalized)) : 0; + } + } else { + waveform.fill(50); + } + + this.logger.debug(`Generated waveform with ${waveform.length} values`); + + return { seconds, waveform }; + } catch (error) { + this.logger.warn(`Failed to extract audio metadata: ${error.message}, using defaults`); + const defaultWaveform = new Uint8Array(64); + defaultWaveform.fill(50); + return { seconds: 1, waveform: defaultWaveform }; + } + } + public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { const mediaData: SendAudioDto = { ...data }; @@ -3056,9 +3109,13 @@ export class BaileysStartupService extends ChannelStartupService { const convert = await this.processAudio(mediaData.audio); if (Buffer.isBuffer(convert)) { + const { seconds, waveform } = await this.getAudioMetadata(convert); + + const messageContent = { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus', seconds, waveform }; + const result = this.sendMessageWithTyping( data.number, - { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus' }, + messageContent as any, { presence: 'recording', delay: data?.delay }, isIntegration, ); @@ -3069,12 +3126,21 @@ export class BaileysStartupService extends ChannelStartupService { } } + const audioBuffer = isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'); + let metadata: { seconds: number; waveform: Uint8Array } | undefined; + + // Only generate waveform for buffers, not URLs + if (Buffer.isBuffer(audioBuffer)) { + metadata = await this.getAudioMetadata(audioBuffer); + } + return await this.sendMessageWithTyping( data.number, { - audio: isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'), + audio: audioBuffer, ptt: true, mimetype: 'audio/ogg; codecs=opus', + ...(metadata && { seconds: metadata.seconds, waveform: metadata.waveform }), }, { presence: 'recording', delay: data?.delay }, isIntegration, From 8aac7403d43c21931a32300a3ddaca55b577c82a Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:06:44 -0300 Subject: [PATCH 18/71] fix(integrations): resolve typebot media regression and baileys link preview - Fix linkPreview logic in Baileys to default to true - Add support for 'file' and 'embed' types in Typebot integration - Ensure correct media type detection for PDFs and docs --- .../whatsapp/whatsapp.baileys.service.ts | 22 +++++++------- .../typebot/services/typebot.service.ts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 4db8146cc..59619c4c0 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -436,7 +436,7 @@ export class BaileysStartupService extends ChannelStartupService { qrcodeTerminal.generate(qr, { small: true }, (qrcode) => this.logger.log( `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, + qrcode, ), ); @@ -1049,16 +1049,16 @@ export class BaileysStartupService extends ChannelStartupService { const messagesRepository: Set = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? - ( - await this.prismaRepository.message.findMany({ - select: { key: true }, - where: { instanceId: this.instanceId }, - }) - ).map((message) => { - const key = message.key as { id: string }; + ( + await this.prismaRepository.message.findMany({ + select: { key: true }, + where: { instanceId: this.instanceId }, + }) + ).map((message) => { + const key = message.key as { id: string }; - return key.id; - }), + return key.id; + }), ); if (chatwootImport.getRepositoryMessagesCache(instance) === null) { @@ -2432,7 +2432,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - const linkPreview = options?.linkPreview != false ? undefined : false; + const linkPreview = options?.linkPreview !== false; let quoted: WAMessage; diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index 03712bfdb..b18b338bb 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -368,6 +368,36 @@ export class TypebotService extends BaseChatbotService { sendTelemetry('/message/sendWhatsAppAudio'); } + if (message.type === 'file' || message.type === 'embed') { + const mediaUrl = message.content.url; + const mediaType = this.getMediaType(mediaUrl); + + if (mediaType === 'audio') { + await instance.audioWhatsapp( + { + number: session.remoteJid, + delay: settings?.delayMessage || 1000, + encoding: true, + audio: mediaUrl, + }, + false, + ); + } else { + await instance.mediaMessage( + { + number: session.remoteJid, + delay: settings?.delayMessage || 1000, + mediatype: mediaType || 'document', + media: mediaUrl, + fileName: message.content.name || 'document.pdf', + }, + null, + false, + ); + } + sendTelemetry('/message/sendMedia'); + } + const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); if (wait) { From 3f91cf41e2e11a91e7a47a5e6c5f824db3d392ce Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:17:56 -0300 Subject: [PATCH 19/71] reverte undefined --- .../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 59619c4c0..7cd6a8905 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2432,7 +2432,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - const linkPreview = options?.linkPreview !== false; + const linkPreview = options?.linkPreview != false ? undefined : false; let quoted: WAMessage; From 53f47d5fff2d5160d2ee0e13cf1408dd4201b63a Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:32:41 -0300 Subject: [PATCH 20/71] =?UTF-8?q?feat:=20=20Implementei=20agora=20a=20gera?= =?UTF-8?q?=C3=A7=C3=A3o=20ativa=20de=20Link=20Preview=20dentro=20do=20ser?= =?UTF-8?q?vi=C3=A7o=20do=20WhatsApp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../whatsapp/whatsapp.baileys.service.ts | 47 ++++++++++++++++++- 1 file changed, 46 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 7cd6a8905..3969af768 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -139,6 +139,7 @@ import { createHash } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; import FormData from 'form-data'; +import { getLinkPreview } from 'link-preview-js'; import Long from 'long'; import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; @@ -2221,6 +2222,43 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async generateLinkPreview(text: string) { + try { + const linkRegex = /https?:\/\/[^\s]+/; + const match = text.match(linkRegex); + + if (!match) return undefined; + + const url = match[0]; + const previewData = await getLinkPreview(url, { + imagesPropertyType: 'og', // fetches only open-graph images + headers: { + 'user-agent': 'googlebot', // fetches with googlebot to prevent login pages + }, + }) as any; + + if (!previewData || !previewData.title) return undefined; + + const image = previewData.images && previewData.images.length > 0 ? previewData.images[0] : undefined; + + return { + externalAdReply: { + title: previewData.title, + body: previewData.description, + mediaType: 2, // 2 for video/image preview, though usually 1 is for thumbnail + thumbnailUrl: image, + sourceUrl: url, + mediaUrl: url, + renderLargerThumbnail: true, + showAdAttribution: true + } + }; + } catch (error) { + this.logger.error(`Error generating link preview: ${error}`); + return undefined; + } + } + private async sendMessage( sender: string, message: any, @@ -2432,7 +2470,12 @@ export class BaileysStartupService extends ChannelStartupService { } } - const linkPreview = options?.linkPreview != false ? undefined : false; + const linkPreview = options?.linkPreview === false ? false : undefined; + + let previewContext: any = undefined; + if (linkPreview !== false && (message as any)?.conversation) { + previewContext = await this.generateLinkPreview((message as any).conversation); + } let quoted: WAMessage; @@ -2486,6 +2529,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted, null, group?.ephemeralDuration, + previewContext, // group?.participants, ); } else { @@ -2499,6 +2543,7 @@ export class BaileysStartupService extends ChannelStartupService { unsigned: false, }, disappearingMode: { initiator: 0 }, + ...previewContext, }; messageSent = await this.sendMessage( sender, From dcc9ffc64f1b153386c48015fe7f20b981cef490 Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:40:04 -0300 Subject: [PATCH 21/71] feat: Fix linkPreview via label --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3969af768..2473c7503 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2249,8 +2249,8 @@ export class BaileysStartupService extends ChannelStartupService { thumbnailUrl: image, sourceUrl: url, mediaUrl: url, - renderLargerThumbnail: true, - showAdAttribution: true + renderLargerThumbnail: true + // showAdAttribution: true // Removed to prevent "Sent via ad" label } }; } catch (error) { From 260ad02472a15bb383539d2482ebd3e43a0aa470 Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:53:51 -0300 Subject: [PATCH 22/71] feat: implement Baileys channel services with advanced message handling, Typebot integration, and comprehensive API functionalities for chat, group, and business profiles. --- .../whatsapp/whatsapp.baileys.service.ts | 5 +++- .../typebot/services/typebot.service.ts | 28 +++++++++++++++++-- 2 files changed, 30 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 2473c7503..2068096f9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2229,7 +2229,10 @@ export class BaileysStartupService extends ChannelStartupService { if (!match) return undefined; - const url = match[0]; + // Trim common trailing punctuation that may follow URLs in natural text + const url = match[0].replace(/[.,);\]]+$/u, ''); + if (!url) return undefined; + const previewData = await getLinkPreview(url, { imagesPropertyType: 'og', // fetches only open-graph images headers: { diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index b18b338bb..79f3180b8 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -369,9 +369,33 @@ export class TypebotService extends BaseChatbotService { } if (message.type === 'file' || message.type === 'embed') { - const mediaUrl = message.content.url; + const content = message.content as { url?: string; name?: string } | undefined; + if (!content?.url) { + sendTelemetry('/message/sendMediaMissingUrl'); + return; + } + + const mediaUrl = content.url; const mediaType = this.getMediaType(mediaUrl); + let fileName = content.name; + if (!fileName) { + try { + const urlObj = new URL(mediaUrl); + const path = urlObj.pathname || ''; + const candidate = path.split('/').pop() || ''; + if (candidate && candidate.includes('.')) { + fileName = candidate; + } + } catch { + // Ignore URL parsing failures + } + + if (!fileName) { + fileName = mediaType && mediaType !== 'document' ? `media.${mediaType}` : 'attachment'; + } + } + if (mediaType === 'audio') { await instance.audioWhatsapp( { @@ -389,7 +413,7 @@ export class TypebotService extends BaseChatbotService { delay: settings?.delayMessage || 1000, mediatype: mediaType || 'document', media: mediaUrl, - fileName: message.content.name || 'document.pdf', + fileName, }, null, false, From c5df499a265c12241f5d56fba4738f798fdfdaf9 Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sat, 3 Jan 2026 23:59:51 -0300 Subject: [PATCH 23/71] feat: implement Baileys service for WhatsApp channel, managing connections, messages, groups, profiles, and integrating with Chatwoot, OpenAI, and S3. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 13 ++++++++++++- 1 file changed, 12 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 2068096f9..991a632ba 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2244,12 +2244,23 @@ export class BaileysStartupService extends ChannelStartupService { const image = previewData.images && previewData.images.length > 0 ? previewData.images[0] : undefined; + let thumbnail: Buffer | undefined = undefined; + if (image) { + try { + const response = await axios.get(image, { responseType: 'arraybuffer' }); + thumbnail = Buffer.from(response.data); + } catch { + // Ignore image fetch failures + } + } + return { externalAdReply: { title: previewData.title, body: previewData.description, - mediaType: 2, // 2 for video/image preview, though usually 1 is for thumbnail + mediaType: 1, // 1 for image, 2 for video. Using 1 for better cover image support with renderLargerThumbnail thumbnailUrl: image, + thumbnail: thumbnail, sourceUrl: url, mediaUrl: url, renderLargerThumbnail: true From 91d01fa126c9875882720ec3b1e9ac3c22d0e6e1 Mon Sep 17 00:00:00 2001 From: eltonciatto Date: Sun, 4 Jan 2026 00:11:28 -0300 Subject: [PATCH 24/71] feat: implement new Baileys features including PTV messages, poll decryption, and channel fetching --- .../channel/whatsapp/whatsapp.baileys.service.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 991a632ba..2068096f9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2244,23 +2244,12 @@ export class BaileysStartupService extends ChannelStartupService { const image = previewData.images && previewData.images.length > 0 ? previewData.images[0] : undefined; - let thumbnail: Buffer | undefined = undefined; - if (image) { - try { - const response = await axios.get(image, { responseType: 'arraybuffer' }); - thumbnail = Buffer.from(response.data); - } catch { - // Ignore image fetch failures - } - } - return { externalAdReply: { title: previewData.title, body: previewData.description, - mediaType: 1, // 1 for image, 2 for video. Using 1 for better cover image support with renderLargerThumbnail + mediaType: 2, // 2 for video/image preview, though usually 1 is for thumbnail thumbnailUrl: image, - thumbnail: thumbnail, sourceUrl: url, mediaUrl: url, renderLargerThumbnail: true From 4adffae140a21739cf733de7d7b849f4afce69ac Mon Sep 17 00:00:00 2001 From: Elton Ciatto <53355283+eltonciatto@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:21:08 -0300 Subject: [PATCH 25/71] Update src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../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 2068096f9..095b46ccc 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2257,7 +2257,7 @@ export class BaileysStartupService extends ChannelStartupService { } }; } catch (error) { - this.logger.error(`Error generating link preview: ${error}`); + this.logger.error('Error generating link preview', error); return undefined; } } From 3514eeb62e2b317722450600a2ca1471b2167fd7 Mon Sep 17 00:00:00 2001 From: Elton Ciatto <53355283+eltonciatto@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:24:42 -0300 Subject: [PATCH 26/71] Reverte --- .../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 095b46ccc..2068096f9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2257,7 +2257,7 @@ export class BaileysStartupService extends ChannelStartupService { } }; } catch (error) { - this.logger.error('Error generating link preview', error); + this.logger.error(`Error generating link preview: ${error}`); return undefined; } } From 59cfe87d60bf0f1cb24ed2d6b94693843b09c9f7 Mon Sep 17 00:00:00 2001 From: GABRIEL-PI Date: Thu, 4 Dec 2025 13:56:51 -0300 Subject: [PATCH 27/71] =?UTF-8?q?fix:=20usar=20Multi-Device=20nativo=20par?= =?UTF-8?q?a=20evitar=20desconex=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy-vps.sh | 66 ++++++++++ docker-compose.local.yaml | 120 ++++++++++++++++++ docker-compose.prod.yaml | 64 ++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 18 +-- 4 files changed, 255 insertions(+), 13 deletions(-) create mode 100644 deploy-vps.sh create mode 100644 docker-compose.local.yaml create mode 100644 docker-compose.prod.yaml diff --git a/deploy-vps.sh b/deploy-vps.sh new file mode 100644 index 000000000..5c5ee61cf --- /dev/null +++ b/deploy-vps.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# =========================================== +# SCRIPT DE DEPLOY - Evolution API Multi-Device +# =========================================== + +set -e + +echo "🚀 Iniciando deploy da Evolution API com Multi-Device fix..." + +# Cores para output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Verificar se está no diretório correto +if [ ! -f "docker-compose.prod.yaml" ]; then + echo -e "${RED}❌ Erro: Execute este script no diretório da Evolution API${NC}" + exit 1 +fi + +# Backup do docker-compose atual (se existir) +if [ -f "docker-compose.yaml" ]; then + echo -e "${YELLOW}📦 Fazendo backup do docker-compose.yaml atual...${NC}" + cp docker-compose.yaml docker-compose.yaml.backup.$(date +%Y%m%d_%H%M%S) +fi + +# Parar containers existentes (mantém volumes) +echo -e "${YELLOW}⏹️ Parando containers existentes...${NC}" +docker compose -f docker-compose.prod.yaml down 2>/dev/null || docker-compose -f docker-compose.prod.yaml down 2>/dev/null || true + +# Build da nova imagem +echo -e "${YELLOW}🔨 Buildando imagem com Multi-Device fix...${NC}" +docker compose -f docker-compose.prod.yaml build --no-cache api + +# Subir containers +echo -e "${YELLOW}🚀 Iniciando containers...${NC}" +docker compose -f docker-compose.prod.yaml up -d + +# Aguardar API iniciar +echo -e "${YELLOW}⏳ Aguardando API iniciar...${NC}" +sleep 10 + +# Verificar status +echo -e "${GREEN}✅ Deploy concluído!${NC}" +echo "" +echo "📊 Status dos containers:" +docker compose -f docker-compose.prod.yaml ps + +echo "" +echo "📋 Últimos logs da API:" +docker compose -f docker-compose.prod.yaml logs api --tail 20 + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}🎉 Evolution API Multi-Device está rodando!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "🔗 Acesse: http://SEU_IP:8080" +echo "📚 Docs: http://SEU_IP:8080/docs" +echo "🖥️ Manager: http://SEU_IP:8080/manager" +echo "" +echo "💡 Para ver logs em tempo real:" +echo " docker compose -f docker-compose.prod.yaml logs -f api" + diff --git a/docker-compose.local.yaml b/docker-compose.local.yaml new file mode 100644 index 000000000..1c68ee407 --- /dev/null +++ b/docker-compose.local.yaml @@ -0,0 +1,120 @@ +version: "3.8" + +services: + api: + container_name: evolution_api_local + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + ports: + - "8080:8080" + volumes: + - evolution_instances:/evolution/instances + networks: + - evolution-local + environment: + # Servidor + - SERVER_NAME=evolution + - SERVER_TYPE=http + - SERVER_PORT=8080 + - SERVER_URL=http://localhost:8080 + - SERVER_DISABLE_DOCS=false + - SERVER_DISABLE_MANAGER=false + + # Banco de dados + - DATABASE_PROVIDER=postgresql + - DATABASE_CONNECTION_URI=postgresql://evolution:evolution123@postgres:5432/evolution + - DATABASE_CONNECTION_CLIENT_NAME=evolution + - DATABASE_SAVE_DATA_INSTANCE=true + - DATABASE_SAVE_DATA_NEW_MESSAGE=true + - DATABASE_SAVE_MESSAGE_UPDATE=true + - DATABASE_SAVE_DATA_CONTACTS=true + - DATABASE_SAVE_DATA_CHATS=true + - DATABASE_SAVE_DATA_HISTORIC=true + - DATABASE_SAVE_DATA_LABELS=true + - DATABASE_SAVE_IS_ON_WHATSAPP=true + - DATABASE_SAVE_IS_ON_WHATSAPP_DAYS=7 + + # Redis + - CACHE_REDIS_ENABLED=true + - CACHE_REDIS_URI=redis://redis:6379 + - CACHE_REDIS_PREFIX_KEY=evolution-cache + - CACHE_REDIS_TTL=604800 + - CACHE_REDIS_SAVE_INSTANCES=true + - CACHE_LOCAL_ENABLED=true + + # Autenticação + - AUTHENTICATION_API_KEY=sua-api-key-aqui + - AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=false + + # Logs + - LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,WEBHOOKS,WEBSOCKET + - LOG_COLOR=true + - LOG_BAILEYS=error + + # Instâncias + - DEL_INSTANCE=false + - DEL_TEMP_INSTANCES=true + + # Idioma + - LANGUAGE=pt-BR + + # WebSocket + - WEBSOCKET_ENABLED=true + - WEBSOCKET_GLOBAL_EVENTS=true + + # QR Code + - QRCODE_LIMIT=30 + - QRCODE_COLOR=#198754 + + # Telemetria + - TELEMETRY_ENABLED=false + + postgres: + container_name: evolution_postgres_local + image: postgres:15-alpine + restart: unless-stopped + environment: + - POSTGRES_DB=evolution + - POSTGRES_USER=evolution + - POSTGRES_PASSWORD=evolution123 + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - evolution-local + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U evolution -d evolution"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + container_name: evolution_redis_local + image: redis:7-alpine + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis_data:/data + networks: + - evolution-local + ports: + - "6379:6379" + +volumes: + evolution_instances: + postgres_data: + redis_data: + +networks: + evolution-local: + name: evolution-local + driver: bridge + diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 000000000..7f05d11ca --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,64 @@ +services: + api: + container_name: evolution_api + # Build local com as alterações do Multi-Device + build: + context: . + dockerfile: Dockerfile + image: evolution-api:v2.3.4-multidevice + restart: always + depends_on: + - redis + - postgres + ports: + - 8080:8080 + volumes: + - evolution_instances:/evolution/instances + networks: + - evolution-net + env_file: + - .env + expose: + - 8080 + + redis: + image: redis:latest + networks: + - evolution-net + container_name: redis + command: > + redis-server --port 6379 --appendonly yes + volumes: + - evolution_redis:/data + ports: + - 6379:6379 + + postgres: + container_name: postgres + image: postgres:15 + networks: + - evolution-net + command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] + restart: always + ports: + - 5432:5432 + environment: + - POSTGRES_USER=caio + - POSTGRES_PASSWORD=caio123 + - POSTGRES_DB=evolution + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - postgres_data:/var/lib/postgresql/data + expose: + - 5432 + +volumes: + evolution_instances: + evolution_redis: + postgres_data: + +networks: + evolution-net: + name: evolution-net + driver: bridge + diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 4db8146cc..6957c2d4d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -67,7 +67,6 @@ import { Chatwoot, ConfigService, configService, - ConfigSessionPhone, Database, Log, Openai, @@ -124,7 +123,6 @@ import makeWASocket, { Product, proto, UserFacingSocketConfig, - WABrowserDescription, WAMediaUpload, WAMessage, WAMessageKey, @@ -143,7 +141,6 @@ import Long from 'long'; import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; import cron from 'node-cron'; -import { release } from 'os'; import { join } from 'path'; import P from 'pino'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; @@ -624,21 +621,16 @@ export class BaileysStartupService extends ChannelStartupService { private async createClient(number?: string): Promise { this.instance.authState = await this.defineAuthState(); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - - let browserOptions = {}; - if (number || this.phoneNumber) { this.phoneNumber = number; this.logger.info(`Phone number: ${number}`); - } else { - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - browserOptions = { browser }; - - this.logger.info(`Browser: ${browser}`); } + // Multi-Device mode: não definimos browser para evitar ser tratado como WebClient + // Isso faz o Baileys usar o modo MD nativo, que não conflita com outras sessões + this.logger.info('Using Multi-Device native mode (no browser identification)'); + // Fetch latest WhatsApp Web version automatically const baileysVersion = await fetchLatestWaWebVersion({}); const version = baileysVersion.version; @@ -697,7 +689,7 @@ export class BaileysStartupService extends ChannelStartupService { msgRetryCounterCache: this.msgRetryCounterCache, generateHighQualityLinkPreview: true, getMessage: async (key) => (await this.getMessage(key)) as Promise, - ...browserOptions, + // Removido browserOptions para usar Multi-Device nativo (não WebClient) markOnlineOnConnect: this.localSettings.alwaysOnline, retryRequestDelayMs: 350, maxMsgRetryCount: 4, From cecc5e4ed6fd9a98aa1d5eec144788b5675d7f15 Mon Sep 17 00:00:00 2001 From: GABRIEL-PI Date: Thu, 4 Dec 2025 14:19:19 -0300 Subject: [PATCH 28/71] =?UTF-8?q?docs:=20adiciona=20documenta=C3=A7=C3=A3o?= =?UTF-8?q?=20do=20deploy=20Multi-Device=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPLOY-MULTIDEVICE-FIX.md | 282 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 DEPLOY-MULTIDEVICE-FIX.md diff --git a/DEPLOY-MULTIDEVICE-FIX.md b/DEPLOY-MULTIDEVICE-FIX.md new file mode 100644 index 000000000..3c8b9a112 --- /dev/null +++ b/DEPLOY-MULTIDEVICE-FIX.md @@ -0,0 +1,282 @@ +# 🚀 Evolution API - Multi-Device Fix + +## 📋 Resumo da Alteração + +**Problema:** A Evolution API estava caindo/desconectando quando o WhatsApp Android estava ativo, porque se identificava como "WebClient" (WhatsApp Web), ocupando o slot de sessão web. + +**Solução:** Remover a identificação de browser para usar o modo Multi-Device nativo do Baileys 7.x, que não conflita com outras sessões. + +--- + +## 🔧 Alteração no Código + +### Arquivo: `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` + +**ANTES (WebClient - CAI):** +```typescript +const session = this.configService.get('CONFIG_SESSION_PHONE'); + +let browserOptions = {}; + +if (number || this.phoneNumber) { + this.phoneNumber = number; + this.logger.info(`Phone number: ${number}`); +} else { + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + browserOptions = { browser }; + this.logger.info(`Browser: ${browser}`); +} + +// ... no socketConfig: +...browserOptions, +``` + +**DEPOIS (Multi-Device nativo - NÃO CAI):** +```typescript +if (number || this.phoneNumber) { + this.phoneNumber = number; + this.logger.info(`Phone number: ${number}`); +} + +// Multi-Device mode: não definimos browser para evitar ser tratado como WebClient +// Isso faz o Baileys usar o modo MD nativo, que não conflita com outras sessões +this.logger.info('Using Multi-Device native mode (no browser identification)'); + +// ... no socketConfig: +// Removido browserOptions para usar Multi-Device nativo (não WebClient) +``` + +### Imports removidos: +- `ConfigSessionPhone` do `@config/env.config` +- `WABrowserDescription` do `baileys` +- `release` do `os` + +--- + +## 📦 Repositório Fork + +**URL:** https://github.com/joinads/evolution-api + +**Commit:** `5dbf3e93` - "fix: usar Multi-Device nativo para evitar desconexões" + +--- + +## 🐳 Deploy na VPS com Docker Compose + +### Pré-requisitos +- Docker e Docker Compose instalados +- Acesso SSH à VPS +- Volumes existentes com dados (PostgreSQL, Redis, Instances) + +### Volumes Utilizados (externos) +``` +evolution-clean_evolution_instances # Dados das instâncias WhatsApp +evolution-clean_evolution_redis # Cache Redis +evolution-clean_postgres_data # Banco de dados PostgreSQL +``` + +--- + +## 📝 Comandos de Deploy + +### 1. Clone o repositório +```bash +cd ~ +git clone https://github.com/joinads/evolution-api.git evolution-api-custom +cd evolution-api-custom +``` + +### 2. Copie o .env existente +```bash +cp ~/evolution-clean/.env . +``` + +### 3. Crie o docker-compose.prod.yaml +```bash +cat > docker-compose.prod.yaml << 'EOF' +services: + api: + container_name: evolution_api + build: + context: . + dockerfile: Dockerfile + image: evolution-api:v2.3.4-multidevice + restart: always + depends_on: + - redis + - postgres + ports: + - 8080:8080 + volumes: + - evolution-clean_evolution_instances:/evolution/instances + networks: + - evolution-net + env_file: + - .env + expose: + - 8080 + + redis: + image: redis:latest + networks: + - evolution-net + container_name: redis + command: > + redis-server --port 6379 --appendonly yes + volumes: + - evolution-clean_evolution_redis:/data + ports: + - 6379:6379 + + postgres: + container_name: postgres + image: postgres:15 + networks: + - evolution-net + command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] + restart: always + ports: + - 5432:5432 + environment: + - POSTGRES_USER=caio + - POSTGRES_PASSWORD=caio123 + - POSTGRES_DB=evolution + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - evolution-clean_postgres_data:/var/lib/postgresql/data + expose: + - 5432 + +volumes: + evolution-clean_evolution_instances: + external: true + evolution-clean_evolution_redis: + external: true + evolution-clean_postgres_data: + external: true + +networks: + evolution-net: + name: evolution-net + driver: bridge +EOF +``` + +### 4. Pare a Evolution antiga (se estiver rodando) +```bash +cd ~/evolution-clean +docker-compose down +``` + +### 5. Build da nova imagem +```bash +cd ~/evolution-api-custom +docker-compose -f docker-compose.prod.yaml build --no-cache +``` + +### 6. Suba os containers +```bash +docker-compose -f docker-compose.prod.yaml up -d +``` + +### 7. Verifique os logs +```bash +docker-compose -f docker-compose.prod.yaml logs -f api +``` + +--- + +## 🔄 Comandos Úteis + +### Ver status dos containers +```bash +docker-compose -f docker-compose.prod.yaml ps +``` + +### Reiniciar a API +```bash +docker-compose -f docker-compose.prod.yaml restart api +``` + +### Ver logs em tempo real +```bash +docker-compose -f docker-compose.prod.yaml logs -f api +``` + +### Parar todos os containers +```bash +docker-compose -f docker-compose.prod.yaml down +``` + +### Rebuild após alterações no código +```bash +git pull origin main +docker-compose -f docker-compose.prod.yaml build --no-cache +docker-compose -f docker-compose.prod.yaml up -d +``` + +--- + +## 🔍 Verificar se o Fix está Funcionando + +Nos logs da API, você deve ver: +``` +Using Multi-Device native mode (no browser identification) +``` + +**NÃO deve mais aparecer:** +``` +Browser: ['Evolution API', 'Chrome', ...] +``` + +--- + +## ⚠️ Notas Importantes + +1. **Instâncias existentes:** Continuam funcionando normalmente. As credenciais salvas não dependem do parâmetro `browser`. + +2. **Novas conexões:** Usarão o modo Multi-Device nativo automaticamente. + +3. **Se uma sessão expirar:** Ao reconectar via QR Code, já usará o novo modo. + +4. **Volumes externos:** O docker-compose usa `external: true` para apontar para os volumes existentes, preservando todos os dados. + +--- + +## 📊 Comparação: Antes vs Depois + +| Aspecto | Antes (v2.3.4 oficial) | Depois (com fix) | +|---------|------------------------|------------------| +| Identificação | `['Evolution API', 'Chrome', OS]` | Nenhuma (MD nativo) | +| Tipo de sessão | WebClient | Multi-Device | +| Aparece como | "WhatsApp Web" | Dispositivo vinculado | +| Conflita com Android | ✅ SIM | ❌ NÃO | +| Cai quando Android ativo | ✅ SIM | ❌ NÃO | + +--- + +## 🆘 Rollback (Voltar para versão oficial) + +Se precisar voltar para a versão oficial: + +```bash +cd ~/evolution-api-custom +docker-compose -f docker-compose.prod.yaml down + +cd ~/evolution-clean +docker-compose up -d +``` + +--- + +## 📅 Data da Alteração +**04 de Dezembro de 2025** + +## 👤 Autor +Alteração realizada com auxílio de IA (Claude/Cursor) + +## 🔗 Links +- Fork: https://github.com/joinads/evolution-api +- Original: https://github.com/EvolutionAPI/evolution-api +- Baileys: https://github.com/WhiskeySockets/Baileys + From 94602050577e8ed2e843588900c041b099686d20 Mon Sep 17 00:00:00 2001 From: GABRIEL-PI Date: Thu, 4 Dec 2025 14:21:20 -0300 Subject: [PATCH 29/71] =?UTF-8?q?docs:=20adiciona=20documenta=C3=A7=C3=A3o?= =?UTF-8?q?=20do=20deploy=20Multi-Device=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPLOY-MULTIDEVICE-FIX.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/DEPLOY-MULTIDEVICE-FIX.md b/DEPLOY-MULTIDEVICE-FIX.md index 3c8b9a112..a9d16076a 100644 --- a/DEPLOY-MULTIDEVICE-FIX.md +++ b/DEPLOY-MULTIDEVICE-FIX.md @@ -267,14 +267,6 @@ cd ~/evolution-clean docker-compose up -d ``` ---- - -## 📅 Data da Alteração -**04 de Dezembro de 2025** - -## 👤 Autor -Alteração realizada com auxílio de IA (Claude/Cursor) - ## 🔗 Links - Fork: https://github.com/joinads/evolution-api - Original: https://github.com/EvolutionAPI/evolution-api From 35ee2fecb79e8f37f10be4ab6db1e5c9d72918bd Mon Sep 17 00:00:00 2001 From: GABRIEL-PI Date: Sat, 10 Jan 2026 13:44:28 -0300 Subject: [PATCH 30/71] chore: remove unused deploy files and cleanup whatsapp service --- DEPLOY-MULTIDEVICE-FIX.md | 274 ------------------ deploy-vps.sh | 66 ----- docker-compose.local.yaml | 120 -------- docker-compose.prod.yaml | 64 ---- .../whatsapp/whatsapp.baileys.service.ts | 6 +- 5 files changed, 4 insertions(+), 526 deletions(-) delete mode 100644 DEPLOY-MULTIDEVICE-FIX.md delete mode 100644 deploy-vps.sh delete mode 100644 docker-compose.local.yaml delete mode 100644 docker-compose.prod.yaml diff --git a/DEPLOY-MULTIDEVICE-FIX.md b/DEPLOY-MULTIDEVICE-FIX.md deleted file mode 100644 index a9d16076a..000000000 --- a/DEPLOY-MULTIDEVICE-FIX.md +++ /dev/null @@ -1,274 +0,0 @@ -# 🚀 Evolution API - Multi-Device Fix - -## 📋 Resumo da Alteração - -**Problema:** A Evolution API estava caindo/desconectando quando o WhatsApp Android estava ativo, porque se identificava como "WebClient" (WhatsApp Web), ocupando o slot de sessão web. - -**Solução:** Remover a identificação de browser para usar o modo Multi-Device nativo do Baileys 7.x, que não conflita com outras sessões. - ---- - -## 🔧 Alteração no Código - -### Arquivo: `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` - -**ANTES (WebClient - CAI):** -```typescript -const session = this.configService.get('CONFIG_SESSION_PHONE'); - -let browserOptions = {}; - -if (number || this.phoneNumber) { - this.phoneNumber = number; - this.logger.info(`Phone number: ${number}`); -} else { - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - browserOptions = { browser }; - this.logger.info(`Browser: ${browser}`); -} - -// ... no socketConfig: -...browserOptions, -``` - -**DEPOIS (Multi-Device nativo - NÃO CAI):** -```typescript -if (number || this.phoneNumber) { - this.phoneNumber = number; - this.logger.info(`Phone number: ${number}`); -} - -// Multi-Device mode: não definimos browser para evitar ser tratado como WebClient -// Isso faz o Baileys usar o modo MD nativo, que não conflita com outras sessões -this.logger.info('Using Multi-Device native mode (no browser identification)'); - -// ... no socketConfig: -// Removido browserOptions para usar Multi-Device nativo (não WebClient) -``` - -### Imports removidos: -- `ConfigSessionPhone` do `@config/env.config` -- `WABrowserDescription` do `baileys` -- `release` do `os` - ---- - -## 📦 Repositório Fork - -**URL:** https://github.com/joinads/evolution-api - -**Commit:** `5dbf3e93` - "fix: usar Multi-Device nativo para evitar desconexões" - ---- - -## 🐳 Deploy na VPS com Docker Compose - -### Pré-requisitos -- Docker e Docker Compose instalados -- Acesso SSH à VPS -- Volumes existentes com dados (PostgreSQL, Redis, Instances) - -### Volumes Utilizados (externos) -``` -evolution-clean_evolution_instances # Dados das instâncias WhatsApp -evolution-clean_evolution_redis # Cache Redis -evolution-clean_postgres_data # Banco de dados PostgreSQL -``` - ---- - -## 📝 Comandos de Deploy - -### 1. Clone o repositório -```bash -cd ~ -git clone https://github.com/joinads/evolution-api.git evolution-api-custom -cd evolution-api-custom -``` - -### 2. Copie o .env existente -```bash -cp ~/evolution-clean/.env . -``` - -### 3. Crie o docker-compose.prod.yaml -```bash -cat > docker-compose.prod.yaml << 'EOF' -services: - api: - container_name: evolution_api - build: - context: . - dockerfile: Dockerfile - image: evolution-api:v2.3.4-multidevice - restart: always - depends_on: - - redis - - postgres - ports: - - 8080:8080 - volumes: - - evolution-clean_evolution_instances:/evolution/instances - networks: - - evolution-net - env_file: - - .env - expose: - - 8080 - - redis: - image: redis:latest - networks: - - evolution-net - container_name: redis - command: > - redis-server --port 6379 --appendonly yes - volumes: - - evolution-clean_evolution_redis:/data - ports: - - 6379:6379 - - postgres: - container_name: postgres - image: postgres:15 - networks: - - evolution-net - command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] - restart: always - ports: - - 5432:5432 - environment: - - POSTGRES_USER=caio - - POSTGRES_PASSWORD=caio123 - - POSTGRES_DB=evolution - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - evolution-clean_postgres_data:/var/lib/postgresql/data - expose: - - 5432 - -volumes: - evolution-clean_evolution_instances: - external: true - evolution-clean_evolution_redis: - external: true - evolution-clean_postgres_data: - external: true - -networks: - evolution-net: - name: evolution-net - driver: bridge -EOF -``` - -### 4. Pare a Evolution antiga (se estiver rodando) -```bash -cd ~/evolution-clean -docker-compose down -``` - -### 5. Build da nova imagem -```bash -cd ~/evolution-api-custom -docker-compose -f docker-compose.prod.yaml build --no-cache -``` - -### 6. Suba os containers -```bash -docker-compose -f docker-compose.prod.yaml up -d -``` - -### 7. Verifique os logs -```bash -docker-compose -f docker-compose.prod.yaml logs -f api -``` - ---- - -## 🔄 Comandos Úteis - -### Ver status dos containers -```bash -docker-compose -f docker-compose.prod.yaml ps -``` - -### Reiniciar a API -```bash -docker-compose -f docker-compose.prod.yaml restart api -``` - -### Ver logs em tempo real -```bash -docker-compose -f docker-compose.prod.yaml logs -f api -``` - -### Parar todos os containers -```bash -docker-compose -f docker-compose.prod.yaml down -``` - -### Rebuild após alterações no código -```bash -git pull origin main -docker-compose -f docker-compose.prod.yaml build --no-cache -docker-compose -f docker-compose.prod.yaml up -d -``` - ---- - -## 🔍 Verificar se o Fix está Funcionando - -Nos logs da API, você deve ver: -``` -Using Multi-Device native mode (no browser identification) -``` - -**NÃO deve mais aparecer:** -``` -Browser: ['Evolution API', 'Chrome', ...] -``` - ---- - -## ⚠️ Notas Importantes - -1. **Instâncias existentes:** Continuam funcionando normalmente. As credenciais salvas não dependem do parâmetro `browser`. - -2. **Novas conexões:** Usarão o modo Multi-Device nativo automaticamente. - -3. **Se uma sessão expirar:** Ao reconectar via QR Code, já usará o novo modo. - -4. **Volumes externos:** O docker-compose usa `external: true` para apontar para os volumes existentes, preservando todos os dados. - ---- - -## 📊 Comparação: Antes vs Depois - -| Aspecto | Antes (v2.3.4 oficial) | Depois (com fix) | -|---------|------------------------|------------------| -| Identificação | `['Evolution API', 'Chrome', OS]` | Nenhuma (MD nativo) | -| Tipo de sessão | WebClient | Multi-Device | -| Aparece como | "WhatsApp Web" | Dispositivo vinculado | -| Conflita com Android | ✅ SIM | ❌ NÃO | -| Cai quando Android ativo | ✅ SIM | ❌ NÃO | - ---- - -## 🆘 Rollback (Voltar para versão oficial) - -Se precisar voltar para a versão oficial: - -```bash -cd ~/evolution-api-custom -docker-compose -f docker-compose.prod.yaml down - -cd ~/evolution-clean -docker-compose up -d -``` - -## 🔗 Links -- Fork: https://github.com/joinads/evolution-api -- Original: https://github.com/EvolutionAPI/evolution-api -- Baileys: https://github.com/WhiskeySockets/Baileys - diff --git a/deploy-vps.sh b/deploy-vps.sh deleted file mode 100644 index 5c5ee61cf..000000000 --- a/deploy-vps.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# =========================================== -# SCRIPT DE DEPLOY - Evolution API Multi-Device -# =========================================== - -set -e - -echo "🚀 Iniciando deploy da Evolution API com Multi-Device fix..." - -# Cores para output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Verificar se está no diretório correto -if [ ! -f "docker-compose.prod.yaml" ]; then - echo -e "${RED}❌ Erro: Execute este script no diretório da Evolution API${NC}" - exit 1 -fi - -# Backup do docker-compose atual (se existir) -if [ -f "docker-compose.yaml" ]; then - echo -e "${YELLOW}📦 Fazendo backup do docker-compose.yaml atual...${NC}" - cp docker-compose.yaml docker-compose.yaml.backup.$(date +%Y%m%d_%H%M%S) -fi - -# Parar containers existentes (mantém volumes) -echo -e "${YELLOW}⏹️ Parando containers existentes...${NC}" -docker compose -f docker-compose.prod.yaml down 2>/dev/null || docker-compose -f docker-compose.prod.yaml down 2>/dev/null || true - -# Build da nova imagem -echo -e "${YELLOW}🔨 Buildando imagem com Multi-Device fix...${NC}" -docker compose -f docker-compose.prod.yaml build --no-cache api - -# Subir containers -echo -e "${YELLOW}🚀 Iniciando containers...${NC}" -docker compose -f docker-compose.prod.yaml up -d - -# Aguardar API iniciar -echo -e "${YELLOW}⏳ Aguardando API iniciar...${NC}" -sleep 10 - -# Verificar status -echo -e "${GREEN}✅ Deploy concluído!${NC}" -echo "" -echo "📊 Status dos containers:" -docker compose -f docker-compose.prod.yaml ps - -echo "" -echo "📋 Últimos logs da API:" -docker compose -f docker-compose.prod.yaml logs api --tail 20 - -echo "" -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN}🎉 Evolution API Multi-Device está rodando!${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" -echo "🔗 Acesse: http://SEU_IP:8080" -echo "📚 Docs: http://SEU_IP:8080/docs" -echo "🖥️ Manager: http://SEU_IP:8080/manager" -echo "" -echo "💡 Para ver logs em tempo real:" -echo " docker compose -f docker-compose.prod.yaml logs -f api" - diff --git a/docker-compose.local.yaml b/docker-compose.local.yaml deleted file mode 100644 index 1c68ee407..000000000 --- a/docker-compose.local.yaml +++ /dev/null @@ -1,120 +0,0 @@ -version: "3.8" - -services: - api: - container_name: evolution_api_local - build: - context: . - dockerfile: Dockerfile - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_started - ports: - - "8080:8080" - volumes: - - evolution_instances:/evolution/instances - networks: - - evolution-local - environment: - # Servidor - - SERVER_NAME=evolution - - SERVER_TYPE=http - - SERVER_PORT=8080 - - SERVER_URL=http://localhost:8080 - - SERVER_DISABLE_DOCS=false - - SERVER_DISABLE_MANAGER=false - - # Banco de dados - - DATABASE_PROVIDER=postgresql - - DATABASE_CONNECTION_URI=postgresql://evolution:evolution123@postgres:5432/evolution - - DATABASE_CONNECTION_CLIENT_NAME=evolution - - DATABASE_SAVE_DATA_INSTANCE=true - - DATABASE_SAVE_DATA_NEW_MESSAGE=true - - DATABASE_SAVE_MESSAGE_UPDATE=true - - DATABASE_SAVE_DATA_CONTACTS=true - - DATABASE_SAVE_DATA_CHATS=true - - DATABASE_SAVE_DATA_HISTORIC=true - - DATABASE_SAVE_DATA_LABELS=true - - DATABASE_SAVE_IS_ON_WHATSAPP=true - - DATABASE_SAVE_IS_ON_WHATSAPP_DAYS=7 - - # Redis - - CACHE_REDIS_ENABLED=true - - CACHE_REDIS_URI=redis://redis:6379 - - CACHE_REDIS_PREFIX_KEY=evolution-cache - - CACHE_REDIS_TTL=604800 - - CACHE_REDIS_SAVE_INSTANCES=true - - CACHE_LOCAL_ENABLED=true - - # Autenticação - - AUTHENTICATION_API_KEY=sua-api-key-aqui - - AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=false - - # Logs - - LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,WEBHOOKS,WEBSOCKET - - LOG_COLOR=true - - LOG_BAILEYS=error - - # Instâncias - - DEL_INSTANCE=false - - DEL_TEMP_INSTANCES=true - - # Idioma - - LANGUAGE=pt-BR - - # WebSocket - - WEBSOCKET_ENABLED=true - - WEBSOCKET_GLOBAL_EVENTS=true - - # QR Code - - QRCODE_LIMIT=30 - - QRCODE_COLOR=#198754 - - # Telemetria - - TELEMETRY_ENABLED=false - - postgres: - container_name: evolution_postgres_local - image: postgres:15-alpine - restart: unless-stopped - environment: - - POSTGRES_DB=evolution - - POSTGRES_USER=evolution - - POSTGRES_PASSWORD=evolution123 - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - evolution-local - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U evolution -d evolution"] - interval: 5s - timeout: 5s - retries: 5 - - redis: - container_name: evolution_redis_local - image: redis:7-alpine - restart: unless-stopped - command: redis-server --appendonly yes - volumes: - - redis_data:/data - networks: - - evolution-local - ports: - - "6379:6379" - -volumes: - evolution_instances: - postgres_data: - redis_data: - -networks: - evolution-local: - name: evolution-local - driver: bridge - diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml deleted file mode 100644 index 7f05d11ca..000000000 --- a/docker-compose.prod.yaml +++ /dev/null @@ -1,64 +0,0 @@ -services: - api: - container_name: evolution_api - # Build local com as alterações do Multi-Device - build: - context: . - dockerfile: Dockerfile - image: evolution-api:v2.3.4-multidevice - restart: always - depends_on: - - redis - - postgres - ports: - - 8080:8080 - volumes: - - evolution_instances:/evolution/instances - networks: - - evolution-net - env_file: - - .env - expose: - - 8080 - - redis: - image: redis:latest - networks: - - evolution-net - container_name: redis - command: > - redis-server --port 6379 --appendonly yes - volumes: - - evolution_redis:/data - ports: - - 6379:6379 - - postgres: - container_name: postgres - image: postgres:15 - networks: - - evolution-net - command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] - restart: always - ports: - - 5432:5432 - environment: - - POSTGRES_USER=caio - - POSTGRES_PASSWORD=caio123 - - POSTGRES_DB=evolution - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - postgres_data:/var/lib/postgresql/data - expose: - - 5432 - -volumes: - evolution_instances: - evolution_redis: - postgres_data: - -networks: - evolution-net: - name: evolution-net - driver: bridge - diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 6957c2d4d..239bf7386 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -621,17 +621,19 @@ export class BaileysStartupService extends ChannelStartupService { private async createClient(number?: string): Promise { this.instance.authState = await this.defineAuthState(); - if (number || this.phoneNumber) { + if (number) { this.phoneNumber = number; - this.logger.info(`Phone number: ${number}`); } +<<<<<<< HEAD // Multi-Device mode: não definimos browser para evitar ser tratado como WebClient // Isso faz o Baileys usar o modo MD nativo, que não conflita com outras sessões this.logger.info('Using Multi-Device native mode (no browser identification)'); // Fetch latest WhatsApp Web version automatically +======= +>>>>>>> 6b18bc21 (chore: remove unused deploy files and cleanup whatsapp service) const baileysVersion = await fetchLatestWaWebVersion({}); const version = baileysVersion.version; From a99960379942d1c23a11a5e5d4e52b0378b18e90 Mon Sep 17 00:00:00 2001 From: GABRIEL-PI Date: Sat, 10 Jan 2026 13:54:45 -0300 Subject: [PATCH 31/71] fix: resolve merge markers in whatsapp service --- .../channel/whatsapp/whatsapp.baileys.service.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 239bf7386..7fc8970a9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -626,14 +626,7 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.info(`Phone number: ${number}`); } -<<<<<<< HEAD - // Multi-Device mode: não definimos browser para evitar ser tratado como WebClient - // Isso faz o Baileys usar o modo MD nativo, que não conflita com outras sessões - this.logger.info('Using Multi-Device native mode (no browser identification)'); - // Fetch latest WhatsApp Web version automatically -======= ->>>>>>> 6b18bc21 (chore: remove unused deploy files and cleanup whatsapp service) const baileysVersion = await fetchLatestWaWebVersion({}); const version = baileysVersion.version; From f07166f945190be67e83cc58908b15fe7a2e8b6d Mon Sep 17 00:00:00 2001 From: Vitor Duggen Date: Wed, 14 Jan 2026 10:08:55 -0300 Subject: [PATCH 32/71] fix(baileys): improve error logging for fetching latest WaWeb version --- .../channel/whatsapp/whatsapp.baileys.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 4db8146cc..52dd0de79 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -646,6 +646,11 @@ export class BaileysStartupService extends ChannelStartupService { const log = `Baileys version: ${version.join('.')}`; this.logger.info(log); + const error = baileysVersion?.error ?? null; + if (error) { + this.logger.error(`Fetch latest WaWeb version error: ${error.message}`); + } + this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`); let options; From f5c8a3d791db17ac69252db7413ee6e99f3edf83 Mon Sep 17 00:00:00 2001 From: Vitor Duggen Date: Wed, 14 Jan 2026 10:18:51 -0300 Subject: [PATCH 33/71] fix(baileys): enhance error logging by serializing error object for fetching latest WaWeb version --- .../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 52dd0de79..358f1aafc 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -648,7 +648,7 @@ export class BaileysStartupService extends ChannelStartupService { const error = baileysVersion?.error ?? null; if (error) { - this.logger.error(`Fetch latest WaWeb version error: ${error.message}`); + this.logger.error(`Fetch latest WaWeb version error: ${JSON.stringify({ error })}`); } this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`); From 5613dd4ec8893e4873fddc526062a3c59fa4e395 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 14 Jan 2026 11:11:48 -0300 Subject: [PATCH 34/71] feat: add generateMessageID method and support for messageId in sendMessage DTO --- src/api/dto/sendMessage.dto.ts | 2 ++ .../evolution/evolution.channel.service.ts | 6 +++++- .../channel/whatsapp/baileys.controller.ts | 6 ++++++ .../channel/whatsapp/baileys.router.ts | 10 ++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 18 ++++++++++++++++-- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index ba9ecf527..797ca111b 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -14,6 +14,7 @@ export class Options { mentionsEveryOne?: boolean; mentioned?: string[]; webhookUrl?: string; + messageId?: string; } export class MediaMessage { @@ -45,6 +46,7 @@ export class Metadata { mentioned?: string[]; encoding?: boolean; notConvertSticker?: boolean; + messageId?: string; } export class SendTextDto extends Metadata { diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 87bea08e6..3d8e632e2 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -318,7 +318,7 @@ export class EvolutionStartupService extends ChannelStartupService { let audioFile; - const messageId = v4(); + const messageId = options?.messageId || v4(); let messageRaw: any; @@ -548,6 +548,7 @@ export class EvolutionStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, null, isIntegration, @@ -613,6 +614,7 @@ export class EvolutionStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, file, isIntegration, @@ -711,6 +713,7 @@ export class EvolutionStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, file, isIntegration, @@ -736,6 +739,7 @@ export class EvolutionStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, null, isIntegration, diff --git a/src/api/integrations/channel/whatsapp/baileys.controller.ts b/src/api/integrations/channel/whatsapp/baileys.controller.ts index ee547338d..5a6e121dd 100644 --- a/src/api/integrations/channel/whatsapp/baileys.controller.ts +++ b/src/api/integrations/channel/whatsapp/baileys.controller.ts @@ -10,6 +10,12 @@ export class BaileysController { return instance.baileysOnWhatsapp(body?.jid); } + public async generateMessageID({ instanceName }: InstanceDto) { + const instance = this.waMonitor.waInstances[instanceName]; + + return instance.generateMessageID(); + } + public async profilePictureUrl({ instanceName }: InstanceDto, body: any) { const instance = this.waMonitor.waInstances[instanceName]; diff --git a/src/api/integrations/channel/whatsapp/baileys.router.ts b/src/api/integrations/channel/whatsapp/baileys.router.ts index 04a1d565f..354995e04 100644 --- a/src/api/integrations/channel/whatsapp/baileys.router.ts +++ b/src/api/integrations/channel/whatsapp/baileys.router.ts @@ -19,6 +19,16 @@ export class BaileysRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) + .get(this.routerPath('generateMessageID'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => baileysController.generateMessageID(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => { const response = await this.dataValidate({ request: req, diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index c87342013..a5435bcc6 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -128,6 +128,7 @@ import makeWASocket, { WAMessageKey, WAPresence, WASocket, + generateMessageIDV2 } from 'baileys'; import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; @@ -1979,6 +1980,13 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async generateMessageID() { + + return { + id: generateMessageIDV2(this.client.user?.id) + }; + } + private async sendMessage( sender: string, message: any, @@ -2242,7 +2250,7 @@ export class BaileysStartupService extends ChannelStartupService { mentions, linkPreview, quoted, - null, + options.messageId, group?.ephemeralDuration, // group?.participants, ); @@ -2264,7 +2272,7 @@ export class BaileysStartupService extends ChannelStartupService { mentions, linkPreview, quoted, - null, + options.messageId, undefined, contextInfo, ); @@ -2490,6 +2498,7 @@ export class BaileysStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, isIntegration, ); @@ -2506,6 +2515,7 @@ export class BaileysStartupService extends ChannelStartupService { linkPreview: data?.linkPreview, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, ); } @@ -2819,6 +2829,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, ); @@ -2841,6 +2852,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, isIntegration, ); @@ -2857,6 +2869,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }; if (file) mediaData.media = file.buffer.toString('base64'); @@ -2872,6 +2885,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted: data?.quoted, mentionsEveryOne: data?.mentionsEveryOne, mentioned: data?.mentioned, + messageId: data?.messageId, }, isIntegration, ); From 04913a8a3e33f4973d74fead391af6e3f46376be Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 14 Jan 2026 11:35:01 -0300 Subject: [PATCH 35/71] Fix lint --- .../channel/whatsapp/whatsapp.baileys.service.ts | 5 ++--- 1 file changed, 2 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 a5435bcc6..5b5c1a51f 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -103,6 +103,7 @@ import makeWASocket, { DisconnectReason, downloadContentFromMessage, downloadMediaMessage, + generateMessageIDV2, generateWAMessageFromContent, getAggregateVotesInPollMessage, GetCatalogOptions, @@ -128,7 +129,6 @@ import makeWASocket, { WAMessageKey, WAPresence, WASocket, - generateMessageIDV2 } from 'baileys'; import { Label } from 'baileys/lib/Types/Label'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; @@ -1981,9 +1981,8 @@ export class BaileysStartupService extends ChannelStartupService { } private async generateMessageID() { - return { - id: generateMessageIDV2(this.client.user?.id) + id: generateMessageIDV2(this.client.user?.id), }; } From ee83aaca3e53257a6cb31f56d205bbc4447e4347 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos <36611481+JefersonRamos@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:35:59 -0300 Subject: [PATCH 36/71] Update src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../channel/whatsapp/whatsapp.baileys.service.ts | 5 ++--- 1 file changed, 2 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 5b5c1a51f..22b0f4694 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1979,10 +1979,9 @@ export class BaileysStartupService extends ChannelStartupService { return error; } } - - private async generateMessageID() { + public generateMessageID() { return { - id: generateMessageIDV2(this.client.user?.id), + id: generateMessageIDV2(this.client.user?.id) }; } From 048f82590ca81de9689eaa96ad2865f69d1c9392 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos <36611481+JefersonRamos@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:36:20 -0300 Subject: [PATCH 37/71] Update src/api/integrations/channel/evolution/evolution.channel.service.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../integrations/channel/evolution/evolution.channel.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 3d8e632e2..76a154fb1 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -318,7 +318,7 @@ export class EvolutionStartupService extends ChannelStartupService { let audioFile; - const messageId = options?.messageId || v4(); + const messageId = options?.messageId ?? v4(); let messageRaw: any; From e5fab3ef2b630804baefc9c161faf6612ccb8aed Mon Sep 17 00:00:00 2001 From: Jeferson Ramos <36611481+JefersonRamos@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:14:03 -0300 Subject: [PATCH 38/71] Update whatsapp.baileys.service.ts --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 22b0f4694..9a6763e16 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -2248,7 +2248,7 @@ export class BaileysStartupService extends ChannelStartupService { mentions, linkPreview, quoted, - options.messageId, + optionsoptions?.messageId ?? null, group?.ephemeralDuration, // group?.participants, ); @@ -2270,7 +2270,7 @@ export class BaileysStartupService extends ChannelStartupService { mentions, linkPreview, quoted, - options.messageId, + options?.messageId ?? null, undefined, contextInfo, ); From 13338df1bce328f9f02bc9906366dcd3f109cf03 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 14 Jan 2026 13:47:14 -0300 Subject: [PATCH 39/71] fix: correct syntax in generateMessageID method --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 9a6763e16..b2c291f01 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1981,7 +1981,7 @@ export class BaileysStartupService extends ChannelStartupService { } public generateMessageID() { return { - id: generateMessageIDV2(this.client.user?.id) + id: generateMessageIDV2(this.client.user?.id), }; } @@ -2248,7 +2248,7 @@ export class BaileysStartupService extends ChannelStartupService { mentions, linkPreview, quoted, - optionsoptions?.messageId ?? null, + options?.messageId ?? null, group?.ephemeralDuration, // group?.participants, ); From afb76b7b35043baeff3bcf4a3f517e1c8757279b Mon Sep 17 00:00:00 2001 From: Vitor Duggen Date: Wed, 14 Jan 2026 20:24:44 -0300 Subject: [PATCH 40/71] feat(baileys): implement caching for WhatsApp Web version fetching - Added caching mechanism to store and retrieve the latest WhatsApp Web version - Enhanced error handling to utilize cached fallback versions when fetching fails - Updated the fetchLatestWaWebVersion function to accept a cache service as an optional parameter --- .../whatsapp/whatsapp.baileys.service.ts | 2 +- src/utils/fetchLatestWaWebVersion.ts | 60 +++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 358f1aafc..32d66fcb6 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -640,7 +640,7 @@ export class BaileysStartupService extends ChannelStartupService { } // Fetch latest WhatsApp Web version automatically - const baileysVersion = await fetchLatestWaWebVersion({}); + const baileysVersion = await fetchLatestWaWebVersion({}, this.cache); const version = baileysVersion.version; const log = `Baileys version: ${version.join('.')}`; diff --git a/src/utils/fetchLatestWaWebVersion.ts b/src/utils/fetchLatestWaWebVersion.ts index f6b0aa6d6..f973f20f2 100644 --- a/src/utils/fetchLatestWaWebVersion.ts +++ b/src/utils/fetchLatestWaWebVersion.ts @@ -1,9 +1,20 @@ import axios, { AxiosRequestConfig } from 'axios'; import { fetchLatestBaileysVersion, WAVersion } from 'baileys'; +import { CacheService } from '../api/services/cache.service'; +import { CacheEngine } from '../cache/cacheengine'; import { Baileys, configService } from '../config/env.config'; -export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => { +// Cache keys +const CACHE_KEY_WHATSAPP_WEB_VERSION = 'whatsapp_web_version'; +const CACHE_KEY_BAILEYS_FALLBACK_VERSION = 'baileys_fallback_version'; + +// Cache TTL (1 hour in seconds) +const CACHE_TTL_SECONDS = 3600; + +const MODULE_NAME = 'whatsapp-version'; + +export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>, cache?: CacheService) => { // Check if manual version is set via configuration const baileysConfig = configService.get('BAILEYS'); const manualVersion = baileysConfig?.VERSION; @@ -19,6 +30,22 @@ export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) = } } + let versionCache = cache || null; + + if (!versionCache) { + // Cache estático para versões do WhatsApp Web e fallback do Baileys (fallback se não for passado via parâmetro) + const cacheEngine = new CacheEngine(configService, MODULE_NAME); + const engine = cacheEngine.getEngine(); + const defaultVersionCache = new CacheService(engine); + versionCache = defaultVersionCache; + } + + // Check cache for WhatsApp Web version + const cachedWaVersion = await versionCache.get(CACHE_KEY_WHATSAPP_WEB_VERSION); + if (cachedWaVersion) { + return cachedWaVersion; + } + try { const { data } = await axios.get('https://web.whatsapp.com/sw.js', { ...options, @@ -29,26 +56,51 @@ export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) = const match = data.match(regex); if (!match?.[1]) { - return { + // Check cache for Baileys fallback version + const cachedFallback = await versionCache.get(CACHE_KEY_BAILEYS_FALLBACK_VERSION); + if (cachedFallback) { + return cachedFallback; + } + + // Fetch and cache Baileys fallback version + const fallbackVersion = { version: (await fetchLatestBaileysVersion()).version as WAVersion, isLatest: false, error: { message: 'Could not find client revision in the fetched content', }, }; + + await versionCache.set(CACHE_KEY_BAILEYS_FALLBACK_VERSION, fallbackVersion, CACHE_TTL_SECONDS); + return fallbackVersion; } const clientRevision = match[1]; - return { + const result = { version: [2, 3000, +clientRevision] as WAVersion, isLatest: true, }; + + // Cache the successful result + await versionCache.set(CACHE_KEY_WHATSAPP_WEB_VERSION, result, CACHE_TTL_SECONDS); + + return result; } catch (error) { - return { + // Check cache for Baileys fallback version + const cachedFallback = await versionCache.get(CACHE_KEY_BAILEYS_FALLBACK_VERSION); + if (cachedFallback) { + return cachedFallback; + } + + // Fetch and cache Baileys fallback version + const fallbackVersion = { version: (await fetchLatestBaileysVersion()).version as WAVersion, isLatest: false, error, }; + + await versionCache.set(CACHE_KEY_BAILEYS_FALLBACK_VERSION, fallbackVersion, CACHE_TTL_SECONDS); + return fallbackVersion; } }; From 3d253fff3ef2e7dacb27dae79acf7f0aed5ff216 Mon Sep 17 00:00:00 2001 From: "felipe.francca" Date: Fri, 16 Jan 2026 09:10:05 -0300 Subject: [PATCH 41/71] =?UTF-8?q?Corre=C3=A7=C3=A3o=20de=20bug:=20Loop=20i?= =?UTF-8?q?nfinito=20de=20reconex=C3=A3o=20do=20c=C3=B3digo=20QR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../channel/whatsapp/whatsapp.baileys.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..8f941a2ec 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -426,8 +426,19 @@ export class BaileysStartupService extends ChannelStartupService { if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; + + // FIX: Não reconectar se é primeira conexão (aguardando QR code) + // Isso evita loop infinito que impede geração do QR + const isInitialConnection = !this.instance.wuid && this.instance.qrcode.count === 0; + + if (isInitialConnection) { + this.logger.info('Initial connection closed, waiting for QR code generation...'); + return; + } + const shouldReconnect = !codesToNotReconnect.includes(statusCode); if (shouldReconnect) { + this.logger.warn(`Connection lost (status: ${statusCode}), reconnecting...`); await this.connectToWhatsapp(this.phoneNumber); } else { this.sendDataWebhook(Events.STATUS_INSTANCE, { From 69e7403ded5691a5189e0d87e09c542e51ea1bb8 Mon Sep 17 00:00:00 2001 From: "felipe.francca" Date: Mon, 19 Jan 2026 15:14:55 -0300 Subject: [PATCH 42/71] refactor: apply code review suggestions (safety checks and logging) --- .../channel/whatsapp/whatsapp.baileys.service.ts | 9 ++++++--- 1 file changed, 6 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 8f941a2ec..f32d4a0e4 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -427,9 +427,9 @@ export class BaileysStartupService extends ChannelStartupService { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; - // FIX: Não reconectar se é primeira conexão (aguardando QR code) - // Isso evita loop infinito que impede geração do QR - const isInitialConnection = !this.instance.wuid && this.instance.qrcode.count === 0; + // FIX: Do not reconnect if it's the initial connection (waiting for QR code) + // This prevents infinite loop that blocks QR code generation + const isInitialConnection = !this.instance.wuid && (this.instance.qrcode?.count ?? 0) === 0; if (isInitialConnection) { this.logger.info('Initial connection closed, waiting for QR code generation...'); @@ -441,6 +441,9 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.warn(`Connection lost (status: ${statusCode}), reconnecting...`); await this.connectToWhatsapp(this.phoneNumber); } else { + this.logger.info( + `Skipping reconnection for status code ${statusCode} (code is in codesToNotReconnect list)`, + ); this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, status: 'closed', From 70d334b0b604b8b08170bc7a88e46057051231b0 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Tue, 20 Jan 2026 10:45:58 -0300 Subject: [PATCH 43/71] fix(whatsapp): enhance contact and chat handling with improved JID mapping and debug logging --- .../whatsapp/whatsapp.baileys.service.ts | 47 +++++++++++++++++-- 1 file changed, 44 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 60e857fcc..9b8f31f7a 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -939,6 +939,14 @@ export class BaileysStartupService extends ChannelStartupService { progress?: number; syncType?: proto.HistorySync.HistorySyncType; }) => { + //These logs are crucial; when something changes in Baileys/WhatsApp, we can more easily understand what changed! + this.logger.debug('Messages abaixo'); + this.logger.debug(messages); + this.logger.debug('Chats abaixo'); + this.logger.debug(chats); + this.logger.debug('Contatos abaixo'); + this.logger.debug(contacts); + try { if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { console.log('received on-demand history sync, messages=', messages); @@ -967,14 +975,30 @@ export class BaileysStartupService extends ChannelStartupService { } const contactsMap = new Map(); + const contactsMapLidJid = new Map(); for (const contact of contacts) { + + let jid = null; + + if (contact?.id?.search('@lid') !== -1) { + if (contact.phoneNumber) { + jid = contact.phoneNumber; + } + } + + if (!jid) { + jid = contact?.id; + } + if (contact.id && (contact.notify || contact.name)) { - contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid: contact.id }); + contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid }); } + + contactsMapLidJid.set(contact.id, { jid }); } - const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = []; + const chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; const chatsRepository = new Set( (await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId } })).map( (chat) => chat.remoteJid, @@ -986,7 +1010,24 @@ export class BaileysStartupService extends ChannelStartupService { continue; } - chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name }); + let remoteJid = null; + let remoteLid = null; + + if (chat.id.search('@lid') !== -1) { + const contact = contactsMapLidJid.get(chat.id); + + remoteLid = chat.id; + + if (contact && contact.jid) { + remoteJid = contact.jid; + } + } + + if (!remoteJid) { + remoteJid = chat.id; + } + + chatsRaw.push({ remoteJid, remoteLid, instanceId: this.instanceId, name: chat.name }); } this.sendDataWebhook(Events.CHATS_SET, chatsRaw); From 3979a9e08ca1eb6e53c66abee41a1de118a1a120 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Tue, 20 Jan 2026 10:53:06 -0300 Subject: [PATCH 44/71] fix(whatsapp): remove unnecessary blank line in contacts mapping loop --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 1 - 1 file changed, 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 9b8f31f7a..7ed2750ca 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -978,7 +978,6 @@ export class BaileysStartupService extends ChannelStartupService { const contactsMapLidJid = new Map(); for (const contact of contacts) { - let jid = null; if (contact?.id?.search('@lid') !== -1) { From 06b0ff0e27903d3811c725488ddb8c4b8e691292 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Tue, 20 Jan 2026 13:14:46 -0300 Subject: [PATCH 45/71] fix(whatsapp): update chatsRaw handling to remove remoteLid and optimize variable declaration --- .../channel/whatsapp/whatsapp.baileys.service.ts | 8 +++++++- 1 file changed, 7 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 7ed2750ca..eb23a7a44 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -997,7 +997,7 @@ export class BaileysStartupService extends ChannelStartupService { contactsMapLidJid.set(contact.id, { jid }); } - const chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; + let chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; const chatsRepository = new Set( (await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId } })).map( (chat) => chat.remoteJid, @@ -1032,6 +1032,12 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CHATS_SET, chatsRaw); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { + chatsRaw = chatsRaw.map((chat) => { + delete chat.remoteLid; + + return chat; + }); + await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); } From d131e83d08cbb366bb710a1f0a1eba0f802f78fb Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 21 Jan 2026 09:53:28 -0300 Subject: [PATCH 46/71] fix(whatsapp): handle remoteLid assignment for chats with accountLid containing '@lid' --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index eb23a7a44..589719dbe 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1022,6 +1022,10 @@ export class BaileysStartupService extends ChannelStartupService { } } + if (!remoteLid && chat.accountLid.search('@lid') !== -1) { + remoteLid = chat.accountLid; + } + if (!remoteJid) { remoteJid = chat.id; } From b8d6c87360f189c41d6f8223d43a56c2e1f146d3 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 21 Jan 2026 17:39:19 -0300 Subject: [PATCH 47/71] fix(whatsapp): correct remoteJid handling and optimize chat creation logic --- .../whatsapp/whatsapp.baileys.service.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 589719dbe..f8384c7b1 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1028,7 +1028,7 @@ export class BaileysStartupService extends ChannelStartupService { if (!remoteJid) { remoteJid = chat.id; - } + } chatsRaw.push({ remoteJid, remoteLid, instanceId: this.instanceId, name: chat.name }); } @@ -1036,13 +1036,13 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CHATS_SET, chatsRaw); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - chatsRaw = chatsRaw.map((chat) => { - delete chat.remoteLid; + const chatsToCreateMany = JSON.parse(JSON.stringify(chatsRaw)).map((chat) => { + delete chat.remoteLid; return chat; - }); + }) - await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); + await this.prismaRepository.chat.createMany({ data: chatsToCreateMany, skipDuplicates: true }); } const messagesRaw: any[] = []; @@ -1525,8 +1525,15 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(messageRaw); sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); + if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { + + const lid = messageRaw.key.remoteJid + messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; + messageRaw.key.remoteJidAlt = lid + + messageRaw.key.addressingMode = 'pn' } console.log(messageRaw); From 9401216927910fc439558e4586f9def9e4d4a308 Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Wed, 21 Jan 2026 17:42:50 -0300 Subject: [PATCH 48/71] fix(whatsapp): lint --- .../channel/whatsapp/whatsapp.baileys.service.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index f8384c7b1..9db4bfcb3 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -997,7 +997,7 @@ export class BaileysStartupService extends ChannelStartupService { contactsMapLidJid.set(contact.id, { jid }); } - let chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; + const chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = []; const chatsRepository = new Set( (await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId } })).map( (chat) => chat.remoteJid, @@ -1028,7 +1028,7 @@ export class BaileysStartupService extends ChannelStartupService { if (!remoteJid) { remoteJid = chat.id; - } + } chatsRaw.push({ remoteJid, remoteLid, instanceId: this.instanceId, name: chat.name }); } @@ -1036,11 +1036,10 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CHATS_SET, chatsRaw); if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - const chatsToCreateMany = JSON.parse(JSON.stringify(chatsRaw)).map((chat) => { delete chat.remoteLid; return chat; - }) + }); await this.prismaRepository.chat.createMany({ data: chatsToCreateMany, skipDuplicates: true }); } @@ -1527,13 +1526,12 @@ export class BaileysStartupService extends ChannelStartupService { sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { - - const lid = messageRaw.key.remoteJid + const lid = messageRaw.key.remoteJid; messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; - messageRaw.key.remoteJidAlt = lid + messageRaw.key.remoteJidAlt = lid; - messageRaw.key.addressingMode = 'pn' + messageRaw.key.addressingMode = 'pn'; } console.log(messageRaw); From 636b9a8d067361c86d7a4dce84ea05e07382ea5d Mon Sep 17 00:00:00 2001 From: Jeferson Ramos Date: Thu, 22 Jan 2026 15:08:33 -0300 Subject: [PATCH 49/71] fix(whatsapp): ensure accountLid is defined before checking for '@lid' in remoteLid assignment --- .../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 9db4bfcb3..aa7a45551 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1022,7 +1022,7 @@ export class BaileysStartupService extends ChannelStartupService { } } - if (!remoteLid && chat.accountLid.search('@lid') !== -1) { + if (!remoteLid && chat.accountLid && chat.accountLid.search('@lid') !== -1) { remoteLid = chat.accountLid; } From d15c434b4c9eaff6479e4734a1162651fb2ce0e1 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 26 Jan 2026 13:07:00 -0300 Subject: [PATCH 50/71] fix(baileys): interactive buttons via deviceSentMessage + CTA limits --- .../whatsapp/whatsapp.baileys.service.ts | 155 +++++++++--------- 1 file changed, 81 insertions(+), 74 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..58935c1a1 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3311,91 +3311,46 @@ export class BaileysStartupService extends ChannelStartupService { ]); public async buttonMessage(data: SendButtonsDto) { - if (data.buttons.length === 0) { - throw new BadRequestException('At least one button is required'); - } - - const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); - - const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + if (data.buttons.length === 0) { + throw new BadRequestException('At least one button is required'); + } - const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); + const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); + const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); - if (hasReplyButtons) { - if (data.buttons.length > 3) { - throw new BadRequestException('Maximum of 3 reply buttons allowed'); - } - if (hasOtherButtons) { - throw new BadRequestException('Reply buttons cannot be mixed with other button types'); - } + // Reply rules + if (hasReplyButtons) { + if (data.buttons.length > 3) { + throw new BadRequestException('Maximum of 3 reply buttons allowed'); } - - if (hasPixButton) { - if (data.buttons.length > 1) { - throw new BadRequestException('Only one PIX button is allowed'); - } - if (hasOtherButtons) { - throw new BadRequestException('PIX button cannot be mixed with other button types'); - } - - const message: proto.IMessage = { - viewOnceMessage: { - message: { - interactiveMessage: { - nativeFlowMessage: { - buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), - }, - }, - }, - }, - }; - - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); + if (hasOtherButtons) { + throw new BadRequestException('Reply buttons cannot be mixed with other button types'); } + } - const generate = await (async () => { - if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); - } - })(); + // CTA rules (url/call/copy) - WhatsApp limits to 2 CTAs + if (hasOtherButtons && !hasReplyButtons && !hasPixButton) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed (url/call/copy)'); + } + } - const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; - }); + // PIX rules + if (hasPixButton) { + if (data.buttons.length > 1) { + throw new BadRequestException('Only one PIX button is allowed'); + } + if (hasOtherButtons) { + throw new BadRequestException('PIX button cannot be mixed with other button types'); + } const message: proto.IMessage = { - viewOnceMessage: { + deviceSentMessage: { message: { interactiveMessage: { - body: { - text: (() => { - let t = '*' + data.title + '*'; - if (data?.description) { - t += '\n\n'; - t += data.description; - t += '\n'; - } - return t; - })(), - }, - footer: { text: data?.footer }, - header: (() => { - if (generate?.message?.imageMessage) { - return { - hasMediaAttachment: !!generate.message.imageMessage, - imageMessage: generate.message.imageMessage, - }; - } - })(), nativeFlowMessage: { - buttons: buttons, + buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), }, }, @@ -3412,6 +3367,58 @@ export class BaileysStartupService extends ChannelStartupService { }); } + const generate = await (async () => { + if (data?.thumbnailUrl) { + return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); + } + })(); + + const buttons = data.buttons.map((value) => { + return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; + }); + + const message: proto.IMessage = { + deviceSentMessage: { + message: { + interactiveMessage: { + body: { + text: (() => { + let t = '*' + data.title + '*'; + if (data?.description) { + t += '\n\n'; + t += data.description; + t += '\n'; + } + return t; + })(), + }, + footer: { text: data?.footer }, + header: (() => { + if (generate?.message?.imageMessage) { + return { + hasMediaAttachment: !!generate.message.imageMessage, + imageMessage: generate.message.imageMessage, + }; + } + })(), + nativeFlowMessage: { + buttons, + messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + }, + }, + }, + }, + }; + + return await this.sendMessageWithTyping(data.number, message, { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }); +} + public async locationMessage(data: SendLocationDto) { return await this.sendMessageWithTyping( data.number, From 08f8d055d4bdd2be807b13e3c9113fd1e3658f43 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 26 Jan 2026 13:19:50 -0300 Subject: [PATCH 51/71] fix(baileys): interactive buttons via deviceSentMessage + CTA limits --- .../whatsapp/whatsapp.baileys.service.ts | 111 +++++++++++------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 58935c1a1..a45bf348d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3310,48 +3310,57 @@ export class BaileysStartupService extends ChannelStartupService { ['random', 'EVP'], ]); - public async buttonMessage(data: SendButtonsDto) { - if (data.buttons.length === 0) { + aqui? + +public async buttonMessage(data: SendButtonsDto) { + if (!data.buttons || data.buttons.length === 0) { throw new BadRequestException('At least one button is required'); } const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); - const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); + const hasCTAButtons = data.buttons.some( + (btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy', + ); + + /* ========================= + * REGRAS DE VALIDAÇÃO + * ========================= */ - // Reply rules + // Reply if (hasReplyButtons) { if (data.buttons.length > 3) { throw new BadRequestException('Maximum of 3 reply buttons allowed'); } - if (hasOtherButtons) { - throw new BadRequestException('Reply buttons cannot be mixed with other button types'); - } - } - - // CTA rules (url/call/copy) - WhatsApp limits to 2 CTAs - if (hasOtherButtons && !hasReplyButtons && !hasPixButton) { - if (data.buttons.length > 2) { - throw new BadRequestException('Maximum of 2 CTA buttons allowed (url/call/copy)'); + if (hasCTAButtons || hasPixButton) { + throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); } } - // PIX rules + // PIX if (hasPixButton) { if (data.buttons.length > 1) { throw new BadRequestException('Only one PIX button is allowed'); } - if (hasOtherButtons) { + if (hasReplyButtons || hasCTAButtons) { throw new BadRequestException('PIX button cannot be mixed with other button types'); } const message: proto.IMessage = { - deviceSentMessage: { + viewOnceMessage: { message: { interactiveMessage: { nativeFlowMessage: { - buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + buttons: [ + { + name: this.mapType.get('pix'), + buttonParamsJson: this.toJSONString(data.buttons[0]), + }, + ], + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, @@ -3367,43 +3376,63 @@ export class BaileysStartupService extends ChannelStartupService { }); } - const generate = await (async () => { - if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); + // CTA (url / call / copy) + if (hasCTAButtons) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed'); + } + if (hasReplyButtons) { + throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); } - })(); + } - const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; - }); + /* ========================= + * HEADER (opcional) + * ========================= */ + + const generatedMedia = data?.thumbnailUrl + ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) + : null; + + /* ========================= + * BOTÕES + * ========================= */ + + const buttons = data.buttons.map((btn) => ({ + name: this.mapType.get(btn.type), + buttonParamsJson: this.toJSONString(btn), + })); + + /* ========================= + * MENSAGEM FINAL + * ========================= */ const message: proto.IMessage = { - deviceSentMessage: { + viewOnceMessage: { message: { interactiveMessage: { body: { text: (() => { - let t = '*' + data.title + '*'; + let text = `*${data.title}*`; if (data?.description) { - t += '\n\n'; - t += data.description; - t += '\n'; + text += `\n\n${data.description}`; } - return t; + return text; })(), }, - footer: { text: data?.footer }, - header: (() => { - if (generate?.message?.imageMessage) { - return { - hasMediaAttachment: !!generate.message.imageMessage, - imageMessage: generate.message.imageMessage, - }; - } - })(), + footer: data?.footer ? { text: data.footer } : undefined, + header: generatedMedia?.message?.imageMessage + ? { + hasMediaAttachment: true, + imageMessage: generatedMedia.message.imageMessage, + } + : undefined, nativeFlowMessage: { buttons, - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, From a4f8e95d1829ea1a2bcd7dc3c8002265b6ac0ed3 Mon Sep 17 00:00:00 2001 From: Bruno Fernandes Date: Mon, 26 Jan 2026 13:55:41 -0300 Subject: [PATCH 52/71] chore(lint): fix formatting and remove stray text --- .../whatsapp/whatsapp.baileys.service.ts | 208 +++++++++--------- 1 file changed, 102 insertions(+), 106 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index a45bf348d..f18795c54 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3310,53 +3310,121 @@ export class BaileysStartupService extends ChannelStartupService { ['random', 'EVP'], ]); - aqui? - -public async buttonMessage(data: SendButtonsDto) { - if (!data.buttons || data.buttons.length === 0) { - throw new BadRequestException('At least one button is required'); - } + public async buttonMessage(data: SendButtonsDto) { + if (!data.buttons || data.buttons.length === 0) { + throw new BadRequestException('At least one button is required'); + } - const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); - const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); - const hasCTAButtons = data.buttons.some( - (btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy', - ); + const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); + const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + const hasCTAButtons = data.buttons.some((btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy'); - /* ========================= - * REGRAS DE VALIDAÇÃO - * ========================= */ + /* ========================= + * REGRAS DE VALIDAÇÃO + * ========================= */ - // Reply - if (hasReplyButtons) { - if (data.buttons.length > 3) { - throw new BadRequestException('Maximum of 3 reply buttons allowed'); - } - if (hasCTAButtons || hasPixButton) { - throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); + // Reply + if (hasReplyButtons) { + if (data.buttons.length > 3) { + throw new BadRequestException('Maximum of 3 reply buttons allowed'); + } + if (hasCTAButtons || hasPixButton) { + throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); + } } - } - // PIX - if (hasPixButton) { - if (data.buttons.length > 1) { - throw new BadRequestException('Only one PIX button is allowed'); + // PIX + if (hasPixButton) { + if (data.buttons.length > 1) { + throw new BadRequestException('Only one PIX button is allowed'); + } + if (hasReplyButtons || hasCTAButtons) { + throw new BadRequestException('PIX button cannot be mixed with other button types'); + } + + const message: proto.IMessage = { + viewOnceMessage: { + message: { + interactiveMessage: { + nativeFlowMessage: { + buttons: [ + { + name: this.mapType.get('pix'), + buttonParamsJson: this.toJSONString(data.buttons[0]), + }, + ], + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), + }, + }, + }, + }, + }; + + return await this.sendMessageWithTyping(data.number, message, { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }); } - if (hasReplyButtons || hasCTAButtons) { - throw new BadRequestException('PIX button cannot be mixed with other button types'); + + // CTA (url / call / copy) + if (hasCTAButtons) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed'); + } + if (hasReplyButtons) { + throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); + } } + /* ========================= + * HEADER (opcional) + * ========================= */ + + const generatedMedia = data?.thumbnailUrl + ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) + : null; + + /* ========================= + * BOTÕES + * ========================= */ + + const buttons = data.buttons.map((btn) => ({ + name: this.mapType.get(btn.type), + buttonParamsJson: this.toJSONString(btn), + })); + + /* ========================= + * MENSAGEM FINAL + * ========================= */ + const message: proto.IMessage = { viewOnceMessage: { message: { interactiveMessage: { + body: { + text: (() => { + let text = `*${data.title}*`; + if (data?.description) { + text += `\n\n${data.description}`; + } + return text; + })(), + }, + footer: data?.footer ? { text: data.footer } : undefined, + header: generatedMedia?.message?.imageMessage + ? { + hasMediaAttachment: true, + imageMessage: generatedMedia.message.imageMessage, + } + : undefined, nativeFlowMessage: { - buttons: [ - { - name: this.mapType.get('pix'), - buttonParamsJson: this.toJSONString(data.buttons[0]), - }, - ], + buttons, messageParamsJson: JSON.stringify({ from: 'api', templateId: v4(), @@ -3376,78 +3444,6 @@ public async buttonMessage(data: SendButtonsDto) { }); } - // CTA (url / call / copy) - if (hasCTAButtons) { - if (data.buttons.length > 2) { - throw new BadRequestException('Maximum of 2 CTA buttons allowed'); - } - if (hasReplyButtons) { - throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); - } - } - - /* ========================= - * HEADER (opcional) - * ========================= */ - - const generatedMedia = data?.thumbnailUrl - ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) - : null; - - /* ========================= - * BOTÕES - * ========================= */ - - const buttons = data.buttons.map((btn) => ({ - name: this.mapType.get(btn.type), - buttonParamsJson: this.toJSONString(btn), - })); - - /* ========================= - * MENSAGEM FINAL - * ========================= */ - - const message: proto.IMessage = { - viewOnceMessage: { - message: { - interactiveMessage: { - body: { - text: (() => { - let text = `*${data.title}*`; - if (data?.description) { - text += `\n\n${data.description}`; - } - return text; - })(), - }, - footer: data?.footer ? { text: data.footer } : undefined, - header: generatedMedia?.message?.imageMessage - ? { - hasMediaAttachment: true, - imageMessage: generatedMedia.message.imageMessage, - } - : undefined, - nativeFlowMessage: { - buttons, - messageParamsJson: JSON.stringify({ - from: 'api', - templateId: v4(), - }), - }, - }, - }, - }, - }; - - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); -} - public async locationMessage(data: SendLocationDto) { return await this.sendMessageWithTyping( data.number, From 2d729a3a353aa1c1d71f8e9bd1948d6a90596449 Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Wed, 28 Jan 2026 00:03:27 -0300 Subject: [PATCH 53/71] Enhance getTypeMessage to include orderMessage Added support for orderMessage in getTypeMessage method and updated message formatting for orders. --- .../chatwoot/services/chatwoot.service.ts | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff188..4fbcba24f 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1758,41 +1758,60 @@ export class ChatwootService { } private getTypeMessage(msg: any) { - 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 ? (msg.audioMessage.caption ?? '') : undefined, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: msg.locationMessage, - liveLocationMessage: msg.liveLocationMessage, - listMessage: msg.listMessage, - listResponseMessage: msg.listResponseMessage, - viewOnceMessageV2: - msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || - msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || - msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, - }; - - return types; - } + 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 ? (msg.audioMessage.caption ?? '') : undefined, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, + listMessage: msg.listMessage, + listResponseMessage: msg.listResponseMessage, + // Adicione a linha abaixo. Atenção à vírgula na linha de cima! + orderMessage: msg.orderMessage, + viewOnceMessageV2: + msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || + msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || + msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, + }; + + return types; +} private getMessageContent(types: any) { const typeKey = Object.keys(types).find((key) => types[key] !== undefined); let result = typeKey ? types[typeKey] : undefined; - // Remove externalAdReplyBody| in Chatwoot (Already Have) + // Remove externalAdReplyBody| in Chatwoot if (result && typeof result === 'string' && result.includes('externalAdReplyBody|')) { result = result.split('externalAdReplyBody|').filter(Boolean).join(''); } + // Tratamento de Pedidos do Catálogo + if (typeKey === 'orderMessage') { + const amount = result.totalAmount1000; + // Converte o objeto Long para número antes da divisão + const rawPrice = (Long.isLong(amount) ? amount.toNumber() : amount) || 0; + const price = (rawPrice / 1000).toLocaleString('pt-BR', { + style: 'currency', + currency: result.totalCurrencyCode || 'BRL', + }); + + return `🛒 *NOVO PEDIDO NO CATÁLOGO*\n\n` + + `*Produto:* ${result.orderTitle}\n` + + `*Valor:* ${price}\n` + + `*ID:* ${result.orderId}\n\n` + + `_Atenda agora para finalizar a venda!_`; + } + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { const latitude = result.degreesLatitude; const longitude = result.degreesLongitude; From be30cdae042be43368e5953a0838d18485721357 Mon Sep 17 00:00:00 2001 From: Santosl2 Date: Wed, 28 Jan 2026 00:51:29 -0300 Subject: [PATCH 54/71] fix(package): add network family autoselection timeout to start:prod script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56e32fcc8..b67d6b00b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc --noEmit && tsup", "start": "tsx ./src/main.ts", - "start:prod": "node dist/main", + "start:prod": "node --network-family-autoselection-attempt-timeout=1000 dist/main", "dev:server": "tsx watch ./src/main.ts", "test": "tsx watch ./test/all.test.ts", "lint": "eslint --fix --ext .ts src", From 6827693c638d1b5ed398146bd9df84b67518bbca Mon Sep 17 00:00:00 2001 From: awanmh Date: Wed, 28 Jan 2026 11:58:36 +0700 Subject: [PATCH 55/71] fix: resolve facebook ads context readability --- src/utils/getConversationMessage.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index eca23b454..0abcaf910 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -16,7 +16,7 @@ const getTypeMessage = (msg: any) => { conversation: msg?.message?.conversation, extendedTextMessage: msg?.message?.extendedTextMessage?.text, contactMessage: msg?.message?.contactMessage?.displayName, - locationMessage: msg?.message?.locationMessage?.degreesLatitude.toString(), + locationMessage: msg?.message?.locationMessage?.degreesLatitude?.toString(), viewOnceMessageV2: msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || @@ -49,9 +49,14 @@ const getTypeMessage = (msg: any) => { : '' }` : undefined, - externalAdReplyBody: msg?.contextInfo?.externalAdReply?.body - ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}` + + // --- FIX FACEBOOK ADS START --- + externalAdReplyBody: msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.body + ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.body}` + : msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.title + ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}` : undefined, + // --- FIX FACEBOOK ADS END --- }; const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown'; @@ -60,7 +65,7 @@ const getTypeMessage = (msg: any) => { }; const getMessageContent = (types: any) => { - const typeKey = Object.keys(types).find((key) => key !== 'externalAdReplyBody' && types[key] !== undefined); + const typeKey = Object.keys(types).find((key) => key !== 'externalAdReplyBody' && key !== 'messageType' && types[key] !== undefined); let result = typeKey ? types[typeKey] : undefined; @@ -73,8 +78,6 @@ const getMessageContent = (types: any) => { export const getConversationMessage = (msg: any) => { const types = getTypeMessage(msg); - const messageContent = getMessageContent(types); - return messageContent; -}; +}; \ No newline at end of file From 2ff572d80ca6e6028538468aca51502c3a72d554 Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Wed, 28 Jan 2026 05:26:42 -0300 Subject: [PATCH 56/71] Enhance order message structure and content Refactor order message handling and improve formatting. --- .../chatwoot/services/chatwoot.service.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 4fbcba24f..560945581 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1774,8 +1774,7 @@ export class ChatwootService { liveLocationMessage: msg.liveLocationMessage, listMessage: msg.listMessage, listResponseMessage: msg.listResponseMessage, - // Adicione a linha abaixo. Atenção à vírgula na linha de cima! - orderMessage: msg.orderMessage, + orderMessage: msg.orderMessage, viewOnceMessageV2: msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || @@ -1795,21 +1794,40 @@ export class ChatwootService { result = result.split('externalAdReplyBody|').filter(Boolean).join(''); } - // Tratamento de Pedidos do Catálogo + // Tratamento de Pedidos do Catálogo (WhatsApp Business Catalog) if (typeKey === 'orderMessage') { + // Extrai o valor - pode ser Long, objeto {low, high}, ou número direto + let rawPrice = 0; const amount = result.totalAmount1000; - // Converte o objeto Long para número antes da divisão - const rawPrice = (Long.isLong(amount) ? amount.toNumber() : amount) || 0; + + if (Long.isLong(amount)) { + rawPrice = amount.toNumber(); + } else if (amount && typeof amount === 'object' && 'low' in amount) { + // Formato {low: number, high: number, unsigned: boolean} + rawPrice = Long.fromValue(amount).toNumber(); + } else if (typeof amount === 'number') { + rawPrice = amount; + } + const price = (rawPrice / 1000).toLocaleString('pt-BR', { style: 'currency', currency: result.totalCurrencyCode || 'BRL', }); - return `🛒 *NOVO PEDIDO NO CATÁLOGO*\n\n` + - `*Produto:* ${result.orderTitle}\n` + - `*Valor:* ${price}\n` + - `*ID:* ${result.orderId}\n\n` + - `_Atenda agora para finalizar a venda!_`; + const itemCount = result.itemCount || 1; + const orderTitle = result.orderTitle || 'Produto do catálogo'; + const orderId = result.orderId || 'N/A'; + + return ( + `🛒 *NOVO PEDIDO NO CATÁLOGO*\n` + + `━━━━━━━━━━━━━━━━━━━━━\n` + + `📦 *Produto:* ${orderTitle}\n` + + `📊 *Quantidade:* ${itemCount}\n` + + `💰 *Total:* ${price}\n` + + `🆔 *Pedido:* #${orderId}\n` + + `━━━━━━━━━━━━━━━━━━━━━\n` + + `_Responda para atender este pedido!_` + ); } if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { From 6c274f71ae444ab241f35017352ad4d07676ed2f Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Wed, 28 Jan 2026 12:18:27 -0300 Subject: [PATCH 57/71] Add caching for processed order IDs to prevent duplicates Implement cache for deduplication of order messages to avoid processing duplicates. --- .../chatwoot/services/chatwoot.service.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 560945581..ff6d28f3f 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -49,6 +49,10 @@ export class ChatwootService { private provider: any; + // Cache para deduplicação de orderMessage (evita mensagens duplicadas) + private processedOrderIds: Map = new Map(); + private readonly ORDER_CACHE_TTL_MS = 30000; // 30 segundos + constructor( private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService, @@ -1795,6 +1799,20 @@ export class ChatwootService { } // Tratamento de Pedidos do Catálogo (WhatsApp Business Catalog) + if (typeKey === 'orderMessage' && result.orderId) { + const now = Date.now(); + // Limpa entradas antigas do cache + this.processedOrderIds.forEach((timestamp, id) => { + if (now - timestamp > this.ORDER_CACHE_TTL_MS) { + this.processedOrderIds.delete(id); + } + }); + // Verifica se já processou este orderId + if (this.processedOrderIds.has(result.orderId)) { + return undefined; // Ignora duplicado + } + this.processedOrderIds.set(result.orderId, now); + } if (typeKey === 'orderMessage') { // Extrai o valor - pode ser Long, objeto {low, high}, ou número direto let rawPrice = 0; From 367153e0b2e2f9f3c3420feded52c4a1d0c800e2 Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Wed, 28 Jan 2026 17:51:22 -0300 Subject: [PATCH 58/71] add support for WhatsApp catalog orderMessage From 22048fe2117c784faf5292dfd69bfb1a5dbabbfb Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Thu, 29 Jan 2026 13:03:06 -0300 Subject: [PATCH 59/71] Refactor message data handling in chatwoot.service.ts Filtered null/undefined values from replyToIds before sending and constructed messageData object to include valid content_attributes. --- .../chatwoot/services/chatwoot.service.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index ff6d28f3f..2a0117aad 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -947,20 +947,39 @@ export class ChatwootService { const sourceReplyId = quotedMsg?.chatwootMessageId || null; + // Filtra valores null/undefined do content_attributes para evitar erro 406 + const filteredReplyToIds = Object.fromEntries( + Object.entries(replyToIds).filter(([_, value]) => value != null) + ); + + // Monta o objeto data, incluindo content_attributes apenas se houver dados válidos + const messageData: any = { + content: content, + message_type: messageType, + content_type: 'text', // Explicitamente define como texto para Chatwoot 4.x + attachments: attachments, + private: privateMessage || false, + }; + + // Adiciona source_id apenas se existir + if (sourceId) { + messageData.source_id = sourceId; + } + + // Adiciona content_attributes apenas se houver dados válidos + if (Object.keys(filteredReplyToIds).length > 0) { + messageData.content_attributes = filteredReplyToIds; + } + + // Adiciona source_reply_id apenas se existir + if (sourceReplyId) { + messageData.source_reply_id = sourceReplyId.toString(); + } + const message = await client.messages.create({ accountId: this.provider.accountId, conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - source_id: sourceId, - content_attributes: { - ...replyToIds, - }, - source_reply_id: sourceReplyId ? sourceReplyId.toString() : null, - }, + data: messageData, }); if (!message) { @@ -1086,11 +1105,14 @@ export class ChatwootService { if (messageBody && instance) { const replyToIds = await this.getReplyToIds(messageBody, instance); - if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { - const content = JSON.stringify({ - ...replyToIds, - }); - data.append('content_attributes', content); + // Filtra valores null/undefined antes de enviar + const filteredReplyToIds = Object.fromEntries( + Object.entries(replyToIds).filter(([_, value]) => value != null) + ); + + if (Object.keys(filteredReplyToIds).length > 0) { + const contentAttrs = JSON.stringify(filteredReplyToIds); + data.append('content_attributes', contentAttrs); } } From 796bc4c89a119cc6d81b5e829b2bf64276012e18 Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Thu, 29 Jan 2026 20:53:23 -0300 Subject: [PATCH 60/71] Implement LID to phone number mapping and caching Added LID to phone number mapping and resolution logic to handle LID addresses. Implemented caching for LID mappings and added methods to clean and save mappings. --- .../chatwoot/services/chatwoot.service.ts | 133 +++++++++++++++++- 1 file changed, 129 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 2a0117aad..b0bd12d2f 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -53,6 +53,10 @@ export class ChatwootService { private processedOrderIds: Map = new Map(); private readonly ORDER_CACHE_TTL_MS = 30000; // 30 segundos + // Cache para mapeamento LID → Número Normal (resolve problema de @lid) + private lidToPhoneMap: Map = new Map(); + private readonly LID_CACHE_TTL_MS = 3600000; // 1 hora + constructor( private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService, @@ -636,10 +640,32 @@ export class ChatwootService { public async createConversation(instance: InstanceDto, body: any) { const isLid = body.key.addressingMode === 'lid'; const isGroup = body.key.remoteJid.endsWith('@g.us'); - const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid; - const { remoteJid } = body.key; - const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; - const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`; + let phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid; + let { remoteJid } = body.key; + + // CORREÇÃO LID: Resolve LID para número normal antes de processar + if (isLid && !isGroup) { + const resolvedPhone = await this.resolveLidToPhone(instance, body.key); + + if (resolvedPhone && resolvedPhone !== remoteJid) { + this.logger.verbose(`LID detected and resolved: ${remoteJid} → ${resolvedPhone}`); + phoneNumber = resolvedPhone; + + // Salva mapeamento se temos remoteJidAlt + if (body.key.remoteJidAlt) { + this.saveLidMapping(remoteJid, body.key.remoteJidAlt); + } + } else if (body.key.remoteJidAlt) { + // Se não resolveu mas tem remoteJidAlt, usa ele + phoneNumber = body.key.remoteJidAlt; + this.saveLidMapping(remoteJid, body.key.remoteJidAlt); + this.logger.verbose(`Using remoteJidAlt for LID: ${remoteJid} → ${phoneNumber}`); + } + } + + // Usa phoneNumber como base para cache (não o LID) + const cacheKey = `${instance.instanceName}:createConversation-${phoneNumber}`; + const lockKey = `${instance.instanceName}:lock:createConversation-${phoneNumber}`; const maxWaitTime = 5000; // 5 seconds const client = await this.clientCw(instance); if (!client) return null; @@ -2070,6 +2096,29 @@ export class ChatwootService { } } + // CORREÇÃO LID: Resolve LID para número normal antes de processar evento + if (body?.key?.remoteJid && body.key.remoteJid.includes('@lid') && !body.key.remoteJid.endsWith('@g.us')) { + const originalJid = body.key.remoteJid; + const resolvedPhone = await this.resolveLidToPhone(instance, body.key); + + if (resolvedPhone && resolvedPhone !== originalJid) { + this.logger.verbose(`Event LID resolved: ${originalJid} → ${resolvedPhone}`); + body.key.remoteJid = resolvedPhone; + + // Salva mapeamento se temos remoteJidAlt + if (body.key.remoteJidAlt) { + this.saveLidMapping(originalJid, body.key.remoteJidAlt); + } + } else if (body.key.remoteJidAlt && !body.key.remoteJidAlt.includes('@lid')) { + // Se não resolveu mas tem remoteJidAlt válido, usa ele + this.logger.verbose(`Using remoteJidAlt for event: ${originalJid} → ${body.key.remoteJidAlt}`); + body.key.remoteJid = body.key.remoteJidAlt; + this.saveLidMapping(originalJid, body.key.remoteJidAlt); + } else { + this.logger.warn(`Could not resolve LID for event, keeping original: ${originalJid}`); + } + } + if (event === 'messages.upsert' || event === 'send.message') { this.logger.info(`[${event}] New message received - Instance: ${JSON.stringify(body, null, 2)}`); if (body.key.remoteJid === 'status@broadcast') { @@ -2614,6 +2663,82 @@ export class ChatwootService { return remoteJid.replace(/:\d+/, '').split('@')[0]; } + /** + * Limpa entradas antigas do cache de mapeamento LID + */ + private cleanLidCache() { + const now = Date.now(); + this.lidToPhoneMap.forEach((value, lid) => { + if (now - value.timestamp > this.LID_CACHE_TTL_MS) { + this.lidToPhoneMap.delete(lid); + } + }); + } + + /** + * Salva mapeamento LID → Número Normal + */ + private saveLidMapping(lid: string, phoneNumber: string) { + if (!lid || !phoneNumber || !lid.includes('@lid')) { + return; + } + + this.cleanLidCache(); + this.lidToPhoneMap.set(lid, { + phone: phoneNumber, + timestamp: Date.now(), + }); + + this.logger.verbose(`LID mapping saved: ${lid} → ${phoneNumber}`); + } + + /** + * Resolve LID para Número Normal + * Retorna o número normal se encontrado, ou o LID original se não encontrado + */ + private async resolveLidToPhone(instance: InstanceDto, messageKey: any): Promise { + const { remoteJid, remoteJidAlt } = messageKey; + + // Se não for LID, retorna o próprio remoteJid + if (!remoteJid || !remoteJid.includes('@lid')) { + return remoteJid; + } + + // 1. Tenta buscar no cache + const cached = this.lidToPhoneMap.get(remoteJid); + if (cached) { + this.logger.verbose(`LID resolved from cache: ${remoteJid} → ${cached.phone}`); + return cached.phone; + } + + // 2. Se tem remoteJidAlt (número alternativo), usa ele e salva no cache + if (remoteJidAlt && !remoteJidAlt.includes('@lid')) { + this.saveLidMapping(remoteJid, remoteJidAlt); + this.logger.verbose(`LID resolved from remoteJidAlt: ${remoteJid} → ${remoteJidAlt}`); + return remoteJidAlt; + } + + // 3. Tenta buscar no banco de dados do Chatwoot + try { + const lidIdentifier = this.normalizeJidIdentifier(remoteJid); + const contact = await this.findContactByIdentifier(instance, lidIdentifier); + + if (contact && contact.phone_number) { + // Converte +554498860240 → 554498860240@s.whatsapp.net + const phoneNumber = contact.phone_number.replace('+', '') + '@s.whatsapp.net'; + this.saveLidMapping(remoteJid, phoneNumber); + this.logger.verbose(`LID resolved from database: ${remoteJid} → ${phoneNumber}`); + return phoneNumber; + } + } catch (error) { + this.logger.warn(`Error resolving LID from database: ${error}`); + } + + // 4. Se não encontrou, retorna null (será necessário criar novo contato) + this.logger.warn(`Could not resolve LID: ${remoteJid}`); + return null; + } + public startImportHistoryMessages(instance: InstanceDto) { if (!this.isImportHistoryAvailable()) { return; From c84626d244db972c0e2624a7ee177d8e4b70c0b1 Mon Sep 17 00:00:00 2001 From: awanmh Date: Fri, 30 Jan 2026 17:00:38 +0700 Subject: [PATCH 61/71] fix: add fallback path for externalAdReply as suggested by sourcery --- src/utils/getConversationMessage.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index 0abcaf910..377df64c1 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -49,13 +49,17 @@ const getTypeMessage = (msg: any) => { : '' }` : undefined, - + // --- FIX FACEBOOK ADS START --- externalAdReplyBody: msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.body ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.body}` : msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.title - ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}` - : undefined, + ? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}` + : msg?.contextInfo?.externalAdReply?.body + ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}` + : msg?.contextInfo?.externalAdReply?.title + ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.title}` + : undefined, // --- FIX FACEBOOK ADS END --- }; @@ -65,7 +69,9 @@ const getTypeMessage = (msg: any) => { }; const getMessageContent = (types: any) => { - const typeKey = Object.keys(types).find((key) => key !== 'externalAdReplyBody' && key !== 'messageType' && types[key] !== undefined); + const typeKey = Object.keys(types).find( + (key) => key !== 'externalAdReplyBody' && key !== 'messageType' && types[key] !== undefined, + ); let result = typeKey ? types[typeKey] : undefined; @@ -80,4 +86,4 @@ export const getConversationMessage = (msg: any) => { const types = getTypeMessage(msg); const messageContent = getMessageContent(types); return messageContent; -}; \ No newline at end of file +}; From cfa475d47beb4e7db1438fe4381e7a63d28a38c1 Mon Sep 17 00:00:00 2001 From: ValdecirMysian Date: Sun, 1 Feb 2026 15:37:20 -0300 Subject: [PATCH 62/71] Implement quoted product message handling Added handling for quoted product messages including price extraction and formatting. --- .../chatwoot/services/chatwoot.service.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index b0bd12d2f..491119627 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1827,6 +1827,7 @@ export class ChatwootService { listMessage: msg.listMessage, listResponseMessage: msg.listResponseMessage, orderMessage: msg.orderMessage, + quotedProductMessage: msg.contextInfo?.quotedMessage?.productMessage, viewOnceMessageV2: msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || @@ -1861,6 +1862,40 @@ export class ChatwootService { } this.processedOrderIds.set(result.orderId, now); } + // Tratamento de Produto citado (WhatsApp Desktop) +if (typeKey === 'quotedProductMessage' && result?.product) { + const product = result.product; + + // Extrai preço + let rawPrice = 0; + const amount = product.priceAmount1000; + + if (Long.isLong(amount)) { + rawPrice = amount.toNumber(); + } else if (amount && typeof amount === 'object' && 'low' in amount) { + rawPrice = Long.fromValue(amount).toNumber(); + } else if (typeof amount === 'number') { + rawPrice = amount; + } + + const price = (rawPrice / 1000).toLocaleString('pt-BR', { + style: 'currency', + currency: product.currencyCode || 'BRL', + }); + + const productTitle = product.title || 'Produto do catálogo'; + const productId = product.productId || 'N/A'; + + return ( + `🛒 *PRODUTO DO CATÁLOGO (Desktop)*\n` + + `━━━━━━━━━━━━━━━━━━━━━\n` + + `📦 *Produto:* ${productTitle}\n` + + `💰 *Preço:* ${price}\n` + + `🆔 *Código:* ${productId}\n` + + `━━━━━━━━━━━━━━━━━━━━━\n` + + `_Cliente perguntou: "${types.conversation || 'Me envia este produto?'}"_` + ); +} if (typeKey === 'orderMessage') { // Extrai o valor - pode ser Long, objeto {low, high}, ou número direto let rawPrice = 0; From 5dd18451ea3c231d55d11871a37cd38712ecc38f Mon Sep 17 00:00:00 2001 From: yurii Date: Mon, 9 Feb 2026 14:44:31 +0100 Subject: [PATCH 63/71] fix(docker): fix docker-compose startup failures for fresh installs - Remove dokploy-network external network dependency that breaks docker-compose up on fresh installs without the network pre-created - Fix evolution-manager frontend crash by adding nginx.conf with corrected gzip_proxied directive (removes invalid must-revalidate value) - Add missing POSTGRES_DATABASE, POSTGRES_USERNAME, POSTGRES_PASSWORD to .env.example (required by docker-compose postgres service) - Fix DATABASE_CONNECTION_URI hostname from postgres to evolution-postgres to match the docker-compose service name Co-Authored-By: Claude Opus 4.6 --- .env.example | 8 ++++++- docker-compose.yaml | 11 +++------- nginx.conf | 51 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 nginx.conf diff --git a/.env.example b/.env.example index 73a3b40d3..f4a99e8b3 100644 --- a/.env.example +++ b/.env.example @@ -52,7 +52,13 @@ DEL_INSTANCE=false # Provider: postgresql | mysql | psql_bouncer DATABASE_PROVIDER=postgresql -DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution_db?schema=evolution_api' +DATABASE_CONNECTION_URI='postgresql://user:pass@evolution-postgres:5432/evolution_db?schema=evolution_api' + +# Postgres container settings (used by docker-compose) +POSTGRES_DATABASE=evolution_db +POSTGRES_USERNAME=user +POSTGRES_PASSWORD=pass + # Client name for the database connection # It is used to separate an API installation from another that uses the same database. DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange diff --git a/docker-compose.yaml b/docker-compose.yaml index e0edee656..a2f526695 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,7 +14,6 @@ services: - evolution_instances:/evolution/instances networks: - evolution-net - - dokploy-network env_file: - .env expose: @@ -26,6 +25,8 @@ services: restart: always ports: - "3000:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/nginx.conf:ro networks: - evolution-net @@ -41,9 +42,6 @@ services: evolution-net: aliases: - evolution-redis - dokploy-network: - aliases: - - evolution-redis expose: - "6379" @@ -67,7 +65,6 @@ services: - postgres_data:/var/lib/postgresql/data networks: - evolution-net - - dokploy-network expose: - "5432" @@ -79,6 +76,4 @@ volumes: networks: evolution-net: name: evolution-net - driver: bridge - dokploy-network: - external: true \ No newline at end of file + driver: bridge \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..7f4036213 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,51 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Handle client routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Cache HTML files for a shorter period + location ~* \.html$ { + expires 1h; + add_header Cache-Control "public"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} From 42b46e0813677f0eab9f7c36b60e07e00cfafcaa Mon Sep 17 00:00:00 2001 From: Milton Sosa Date: Mon, 16 Feb 2026 04:30:13 -0300 Subject: [PATCH 64/71] fix(chatbot): closed session should not block bot re-activation When a chatbot session exists with status='closed', the emit() method returned early, preventing the bot from re-activating on new messages. Root cause: the guard 'if (session.status === closed) return' was meant to skip sessions not awaiting user input, but it also prevented new conversations from starting after a bot flow completed. Fix: nullify the session instead of returning, so processBot enters the '!session' branch and creates a fresh session. Also adds null guards: - getConversationMessage: return empty string instead of undefined - findBotByTrigger: handle null/undefined content gracefully --- src/api/integrations/chatbot/base-chatbot.controller.ts | 6 +++--- src/utils/findBotByTrigger.ts | 8 ++++++++ src/utils/getConversationMessage.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/chatbot/base-chatbot.controller.ts b/src/api/integrations/chatbot/base-chatbot.controller.ts index a5b83e257..4108785e4 100644 --- a/src/api/integrations/chatbot/base-chatbot.controller.ts +++ b/src/api/integrations/chatbot/base-chatbot.controller.ts @@ -797,7 +797,7 @@ export abstract class BaseChatbotController { + const normalizedContent = content?.trim() || ''; + // Check for triggerType 'all' or 'none' (both should match any message) const findTriggerAllOrNone = await botRepository.findFirst({ where: { @@ -16,6 +18,12 @@ export const findBotByTrigger = async (botRepository: any, content: string, inst return findTriggerAllOrNone; } + // If content is empty (null, undefined, whitespace-only, or media-only messages), + // only 'all'/'none' triggers apply — skip keyword/regex matching + if (!normalizedContent) { + return null; + } + const findTriggerAdvanced = await botRepository.findMany({ where: { enabled: true, diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts index eca23b454..bd13f89e2 100644 --- a/src/utils/getConversationMessage.ts +++ b/src/utils/getConversationMessage.ts @@ -76,5 +76,5 @@ export const getConversationMessage = (msg: any) => { const messageContent = getMessageContent(types); - return messageContent; + return messageContent ?? ''; }; From cb4a14d1ef0e279aed5042ec3a569d0cbd678423 Mon Sep 17 00:00:00 2001 From: Milton Sosa Date: Mon, 16 Feb 2026 04:30:40 -0300 Subject: [PATCH 65/71] fix(meta): normalize execution order and fix chatwootIds in Cloud API Two bugs in BusinessStartupService message processing: 1. Execution order: Chatwoot was processed AFTER the bot emit(), but Baileys channel processes Chatwoot FIRST. This inconsistency meant the bot could not access chatwootConversationId/chatwootInboxId when processing messages from the Cloud API. 2. chatwootIds assignment: chatwootInboxId and chatwootConversationId were both incorrectly set to chatwootSentMessage.id instead of .inbox_id and .conversation_id respectively. Fix: reorder to Chatwoot-first (consistent with Baileys) and use the correct property names from the Chatwoot response object. --- .../channel/meta/whatsapp.business.service.ts | 25 ++++++++++--------- 1 file changed, 13 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 1e4808c15..1f820d0e5 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -668,15 +668,7 @@ export class BusinessStartupService extends ChannelStartupService { sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - await chatbotController.emit({ - instance: { instanceName: this.instance.name, instanceId: this.instanceId }, - remoteJid: messageRaw.key.remoteJid, - msg: messageRaw, - pushName: messageRaw.pushName, - }); - + // Normalized order: Chatwoot first, then bot (consistent with Baileys channel) if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, @@ -684,13 +676,22 @@ export class BusinessStartupService extends ChannelStartupService { messageRaw, ); - if (chatwootSentMessage?.id) { + if (chatwootSentMessage) { messageRaw.chatwootMessageId = chatwootSentMessage.id; - messageRaw.chatwootInboxId = chatwootSentMessage.id; - messageRaw.chatwootConversationId = chatwootSentMessage.id; + messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id; + messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id; } } + this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + await chatbotController.emit({ + instance: { instanceName: this.instance.name, instanceId: this.instanceId }, + remoteJid: messageRaw.key.remoteJid, + msg: messageRaw, + pushName: messageRaw.pushName, + }); + if (!this.isMediaMessage(message) && message.type !== 'sticker') { await this.prismaRepository.message.create({ data: messageRaw, From e2a7716e19443c2ab235ea733576114afd3fb285 Mon Sep 17 00:00:00 2001 From: Rafael Berrocal Justiniano Date: Mon, 23 Feb 2026 21:07:21 -0300 Subject: [PATCH 66/71] feat(sqs): add support for custom base_url --- env.example | 1 + src/api/integrations/event/sqs/sqs.controller.ts | 3 ++- src/config/env.config.ts | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/env.example b/env.example index 5fe448b82..d6509d546 100644 --- a/env.example +++ b/env.example @@ -166,6 +166,7 @@ SQS_ACCESS_KEY_ID= SQS_SECRET_ACCESS_KEY= SQS_ACCOUNT_ID= SQS_REGION= +SQS_BASE_URL= SQS_MAX_PAYLOAD_SIZE=1048576 # =========================================== diff --git a/src/api/integrations/event/sqs/sqs.controller.ts b/src/api/integrations/event/sqs/sqs.controller.ts index 2b0398ef2..782a9aa5c 100644 --- a/src/api/integrations/event/sqs/sqs.controller.ts +++ b/src/api/integrations/event/sqs/sqs.controller.ts @@ -126,7 +126,8 @@ export class SqsController extends EventController implements EventControllerInt ? 'singlequeue' : `${event.replace('.', '_').toLowerCase()}`; const queueName = `${prefixName}_${eventFormatted}.fifo`; - const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`; + const baseUrl = sqsConfig.BASE_URL || `https://sqs.${sqsConfig.REGION}.amazonaws.com`; + const sqsUrl = `${baseUrl}/${sqsConfig.ACCOUNT_ID}/${queueName}`; const message = { ...(extra ?? {}), diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7c4e382e7..f1265a875 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -121,6 +121,7 @@ export type Sqs = { SECRET_ACCESS_KEY: string; ACCOUNT_ID: string; REGION: string; + BASE_URL: string; MAX_PAYLOAD_SIZE: number; EVENTS: { APPLICATION_STARTUP: boolean; @@ -585,6 +586,7 @@ export class ConfigService { SECRET_ACCESS_KEY: process.env.SQS_SECRET_ACCESS_KEY || '', ACCOUNT_ID: process.env.SQS_ACCOUNT_ID || '', REGION: process.env.SQS_REGION || '', + BASE_URL: process.env.SQS_BASE_URL || '', MAX_PAYLOAD_SIZE: Number.parseInt(process.env.SQS_MAX_PAYLOAD_SIZE ?? '1048576'), EVENTS: { APPLICATION_STARTUP: process.env?.SQS_GLOBAL_APPLICATION_STARTUP === 'true', From 0419e82d8f2cdd28b16312b94802b3417f11b8ca Mon Sep 17 00:00:00 2001 From: Rafael Berrocal Justiniano Date: Mon, 23 Feb 2026 21:22:19 -0300 Subject: [PATCH 67/71] fix: handle potential trailing slashes --- src/api/integrations/event/sqs/sqs.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/event/sqs/sqs.controller.ts b/src/api/integrations/event/sqs/sqs.controller.ts index 782a9aa5c..d18677c4f 100644 --- a/src/api/integrations/event/sqs/sqs.controller.ts +++ b/src/api/integrations/event/sqs/sqs.controller.ts @@ -126,7 +126,8 @@ export class SqsController extends EventController implements EventControllerInt ? 'singlequeue' : `${event.replace('.', '_').toLowerCase()}`; const queueName = `${prefixName}_${eventFormatted}.fifo`; - const baseUrl = sqsConfig.BASE_URL || `https://sqs.${sqsConfig.REGION}.amazonaws.com`; + const rawBaseUrl = sqsConfig.BASE_URL || `https://sqs.${sqsConfig.REGION}.amazonaws.com`; + const baseUrl = rawBaseUrl.replace(/\/+$/, ''); const sqsUrl = `${baseUrl}/${sqsConfig.ACCOUNT_ID}/${queueName}`; const message = { From ec7999b04f8717cf5dd8bbf42459a1ff161a70f0 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Mon, 23 Feb 2026 21:31:20 +0000 Subject: [PATCH 68/71] feat(history-sync): emit messaging-history.set event on sync completion and fix race condition Reorder webhook emissions (CHATS_SET, MESSAGES_SET) to fire after database persistence, fixing a race condition where consumers received the event before data was queryable. Emit a new MESSAGING_HISTORY_SET event when progress reaches 100%, allowing consumers to know exactly when history sync is complete and messages are available in the database. Register the new event across all transport types (Webhook, WebSocket, RabbitMQ, NATS, SQS, Kafka, Pusher) and validation schemas. --- .../whatsapp/whatsapp.baileys.service.ts | 20 +++++++++++++------ .../integrations/event/event.controller.ts | 1 + src/config/env.config.ts | 10 ++++++++++ src/validate/instance.schema.ts | 4 ++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..9f4a900af 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -989,12 +989,12 @@ export class BaileysStartupService extends ChannelStartupService { chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name }); } - this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); } + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + const messagesRaw: any[] = []; const messagesRepository: Set = new Set( @@ -1046,15 +1046,15 @@ export class BaileysStartupService extends ChannelStartupService { messagesRaw.push(this.prepareMessage(m)); } + if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { + await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); + } + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { isLatest, progress, }); - if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); - } - if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && @@ -1071,6 +1071,14 @@ export class BaileysStartupService extends ChannelStartupService { contacts.filter((c) => !!c.notify || !!c.name).map((c) => ({ id: c.id, name: c.name ?? c.notify })), ); + if (progress === 100) { + this.sendDataWebhook(Events.MESSAGING_HISTORY_SET, { + messageCount: messagesRaw.length, + chatCount: chatsRaw.length, + contactCount: contacts?.length ?? 0, + }); + } + contacts = undefined; messages = undefined; chats = undefined; diff --git a/src/api/integrations/event/event.controller.ts b/src/api/integrations/event/event.controller.ts index 39b52184b..63061ea10 100644 --- a/src/api/integrations/event/event.controller.ts +++ b/src/api/integrations/event/event.controller.ts @@ -162,6 +162,7 @@ export class EventController { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', 'REMOVE_INSTANCE', 'LOGOUT_INSTANCE', 'INSTANCE_CREATE', diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7c4e382e7..772ae9279 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -91,6 +91,7 @@ export type EventsRabbitmq = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; }; export type Rabbitmq = { @@ -150,6 +151,7 @@ export type Sqs = { SEND_MESSAGE: boolean; TYPEBOT_CHANGE_STATUS: boolean; TYPEBOT_START: boolean; + MESSAGING_HISTORY_SET: boolean; }; }; @@ -223,6 +225,7 @@ export type EventsWebhook = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; ERRORS: boolean; ERRORS_WEBHOOK: string; }; @@ -256,6 +259,7 @@ export type EventsPusher = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; }; export type ApiKey = { KEY: string }; @@ -537,6 +541,7 @@ export class ConfigService { CALL: process.env?.RABBITMQ_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.RABBITMQ_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.RABBITMQ_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, NATS: { @@ -574,6 +579,7 @@ export class ConfigService { CALL: process.env?.NATS_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.NATS_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, SQS: { @@ -614,6 +620,7 @@ export class ConfigService { SEND_MESSAGE: process.env?.SQS_GLOBAL_SEND_MESSAGE === 'true', TYPEBOT_CHANGE_STATUS: process.env?.SQS_GLOBAL_TYPEBOT_CHANGE_STATUS === 'true', TYPEBOT_START: process.env?.SQS_GLOBAL_TYPEBOT_START === 'true', + MESSAGING_HISTORY_SET: process.env?.SQS_GLOBAL_MESSAGING_HISTORY_SET === 'true', }, }, KAFKA: { @@ -657,6 +664,7 @@ export class ConfigService { CALL: process.env?.KAFKA_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.KAFKA_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.KAFKA_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.KAFKA_EVENTS_MESSAGING_HISTORY_SET === 'true', }, SASL: process.env?.KAFKA_SASL_ENABLED === 'true' @@ -722,6 +730,7 @@ export class ConfigService { CALL: process.env?.PUSHER_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.PUSHER_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.PUSHER_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.PUSHER_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, WA_BUSINESS: { @@ -779,6 +788,7 @@ export class ConfigService { CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.WEBHOOK_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.WEBHOOK_EVENTS_MESSAGING_HISTORY_SET === 'true', ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true', ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '', }, diff --git a/src/validate/instance.schema.ts b/src/validate/instance.schema.ts index a0553b666..16fd4fe80 100644 --- a/src/validate/instance.schema.ts +++ b/src/validate/instance.schema.ts @@ -86,6 +86,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -123,6 +124,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -160,6 +162,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -197,6 +200,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, From 1242baa5a4bec20f2eced8f1d017186c15d68a4d Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Mon, 23 Feb 2026 21:48:30 +0000 Subject: [PATCH 69/71] fix(history-sync): use cumulative counts in MESSAGING_HISTORY_SET event Track message, chat and contact counts across all history sync batches using instance-level counters, so the final event reports accurate totals instead of only the last batch counts. Addresses Sourcery review feedback on PR #2440. --- .../whatsapp/whatsapp.baileys.service.ts | 24 +++++++++++++++---- 1 file 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 9f4a900af..1c9bd2324 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -252,6 +252,11 @@ export class BaileysStartupService extends ChannelStartupService { private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); + // Cumulative history sync counters (reset on sync completion) + private historySyncMessageCount = 0; + private historySyncChatCount = 0; + private historySyncContactCount = 0; + // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates @@ -993,6 +998,8 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); } + this.historySyncChatCount += chatsRaw.length; + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); const messagesRaw: any[] = []; @@ -1046,6 +1053,8 @@ export class BaileysStartupService extends ChannelStartupService { messagesRaw.push(this.prepareMessage(m)); } + this.historySyncMessageCount += messagesRaw.length; + if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); } @@ -1067,16 +1076,23 @@ export class BaileysStartupService extends ChannelStartupService { ); } + const filteredContacts = contacts.filter((c) => !!c.notify || !!c.name); + this.historySyncContactCount += filteredContacts.length; + await this.contactHandle['contacts.upsert']( - contacts.filter((c) => !!c.notify || !!c.name).map((c) => ({ id: c.id, name: c.name ?? c.notify })), + filteredContacts.map((c) => ({ id: c.id, name: c.name ?? c.notify })), ); if (progress === 100) { this.sendDataWebhook(Events.MESSAGING_HISTORY_SET, { - messageCount: messagesRaw.length, - chatCount: chatsRaw.length, - contactCount: contacts?.length ?? 0, + messageCount: this.historySyncMessageCount, + chatCount: this.historySyncChatCount, + contactCount: this.historySyncContactCount, }); + + this.historySyncMessageCount = 0; + this.historySyncChatCount = 0; + this.historySyncContactCount = 0; } contacts = undefined; From 6f759443b0b6a658ee5144f02790d2ec3d473204 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Tue, 24 Feb 2026 14:06:14 +0000 Subject: [PATCH 70/71] fix(history-sync): reset cumulative counters on new sync start and abort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detect new sync runs by tracking lastProgress — when progress resets or decreases, counters are zeroed before accumulating. This prevents stale counts from aborted syncs leaking into subsequent runs. Addresses Sourcery review feedback on PR #2442. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 12 +++++++++++- 1 file changed, 11 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 1c9bd2324..4b5a115ba 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -252,10 +252,11 @@ export class BaileysStartupService extends ChannelStartupService { private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); - // Cumulative history sync counters (reset on sync completion) + // Cumulative history sync counters (reset on new sync or completion) private historySyncMessageCount = 0; private historySyncChatCount = 0; private historySyncContactCount = 0; + private historySyncLastProgress = -1; // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing @@ -945,6 +946,14 @@ export class BaileysStartupService extends ChannelStartupService { syncType?: proto.HistorySync.HistorySyncType; }) => { try { + // Reset counters when a new sync starts (progress resets or decreases) + if (progress <= this.historySyncLastProgress) { + this.historySyncMessageCount = 0; + this.historySyncChatCount = 0; + this.historySyncContactCount = 0; + } + this.historySyncLastProgress = progress ?? -1; + if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { console.log('received on-demand history sync, messages=', messages); } @@ -1093,6 +1102,7 @@ export class BaileysStartupService extends ChannelStartupService { this.historySyncMessageCount = 0; this.historySyncChatCount = 0; this.historySyncContactCount = 0; + this.historySyncLastProgress = -1; } contacts = undefined; From 21513f5d5c93157f00a77ae983e25c5150c33eeb Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 24 Feb 2026 12:10:06 -0300 Subject: [PATCH 71/71] Remove unnecessary debug logs from sync function Removed debug logging for messages, chats, and contacts. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 25c6dbe09..76280a4d8 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -989,14 +989,6 @@ export class BaileysStartupService extends ChannelStartupService { progress?: number; syncType?: proto.HistorySync.HistorySyncType; }) => { - //These logs are crucial; when something changes in Baileys/WhatsApp, we can more easily understand what changed! - this.logger.debug('Messages abaixo'); - this.logger.debug(messages); - this.logger.debug('Chats abaixo'); - this.logger.debug(chats); - this.logger.debug('Contatos abaixo'); - this.logger.debug(contacts); - try { if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { console.log('received on-demand history sync, messages=', messages);