From a82c2c485229c04f5d09065cac7dc59ccf0a909f Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 19:13:13 -0300 Subject: [PATCH 1/4] fix(meta): handle message_echoes and guard missing contact fields --- .../channel/meta/whatsapp.business.service.ts | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 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..fabb2d305 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -127,19 +127,38 @@ export class BusinessStartupService extends ChannelStartupService { if (!data) return; const content = data.entry[0].changes[0].value; + const firstMessage = + content?.messages?.[0] ?? content?.message_echoes?.[0] ?? content?.smb_message_echoes?.[0] ?? undefined; + const recipient = content?.statuses?.[0]?.recipient_id; + const remoteId = firstMessage?.to ?? firstMessage?.from ?? recipient; try { this.loadChatwoot(); - this.eventHandler(content); + this.eventHandler(this.normalizeWebhookContent(content)); - this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id); + if (remoteId) { + this.phoneNumber = createJid(remoteId); + } } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } + private normalizeWebhookContent(content: any) { + if (!content || typeof content !== 'object') return content; + + const normalized = { ...content }; + const echoes = normalized?.message_echoes ?? normalized?.smb_message_echoes; + + if (!Array.isArray(normalized.messages) && Array.isArray(echoes) && echoes.length > 0) { + normalized.messages = echoes; + } + + return normalized; + } + private async downloadMediaMessage(message: any) { try { const id = message[message.type].id; @@ -386,15 +405,19 @@ export class BusinessStartupService extends ChannelStartupService { try { let messageRaw: any; let pushName: any; + const incomingContact = received?.contacts?.[0]; - if (received.contacts) pushName = received.contacts[0].profile.name; + if (incomingContact) { + pushName = incomingContact?.profile?.name ?? incomingContact?.name ?? incomingContact?.wa_id ?? undefined; + } if (received.messages) { const message = received.messages[0]; // Añadir esta línea para definir message + const remoteJid = createJid(message?.to ?? message?.from); const key = { id: message.id, - remoteJid: this.phoneNumber, + remoteJid, fromMe: message.from === received.metadata.phone_number_id, }; @@ -701,8 +724,11 @@ export class BusinessStartupService extends ChannelStartupService { where: { instanceId: this.instanceId, remoteJid: key.remoteJid }, }); + const contactPhone = incomingContact?.profile?.phone ?? incomingContact?.wa_id ?? message?.to ?? message?.from; + if (!contactPhone) return; + const contactRaw: any = { - remoteJid: received.contacts[0].profile.phone, + remoteJid: createJid(contactPhone), pushName, // profilePicUrl: '', instanceId: this.instanceId, @@ -714,7 +740,7 @@ export class BusinessStartupService extends ChannelStartupService { if (contact) { const contactRaw: any = { - remoteJid: received.contacts[0].profile.phone, + remoteJid: createJid(contactPhone), pushName, // profilePicUrl: '', instanceId: this.instanceId, @@ -745,10 +771,11 @@ export class BusinessStartupService extends ChannelStartupService { } if (received.statuses) { for await (const item of received.statuses) { + const remoteJid = createJid(item?.recipient_id ?? this.phoneNumber); const key = { id: item.id, - remoteJid: this.phoneNumber, - fromMe: this.phoneNumber === received.metadata.phone_number_id, + remoteJid, + fromMe: item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true, }; if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { return; From b8e4bfad40b08bcfd182a871863d520f090d7a60 Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 22:49:03 -0300 Subject: [PATCH 2/4] fix(meta): mark cloud api echoes as fromMe and match display phone --- .../channel/meta/whatsapp.business.service.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index fabb2d305..c966f81a6 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -159,6 +159,26 @@ export class BusinessStartupService extends ChannelStartupService { return normalized; } + private normalizePhoneNumber(value?: string) { + return typeof value === 'string' ? value.replace(/\D/g, '') : ''; + } + + private isCloudApiEchoPayload(received: any) { + return Array.isArray(received?.message_echoes) || Array.isArray(received?.smb_message_echoes); + } + + private isCloudApiFromMe(message: any, received: any) { + if (this.isCloudApiEchoPayload(received)) return true; + + const from = this.normalizePhoneNumber(message?.from); + const displayPhone = this.normalizePhoneNumber(received?.metadata?.display_phone_number); + const phoneNumberId = this.normalizePhoneNumber(received?.metadata?.phone_number_id); + + if (!from) return false; + + return from === displayPhone || from === phoneNumberId; + } + private async downloadMediaMessage(message: any) { try { const id = message[message.type].id; @@ -418,7 +438,7 @@ export class BusinessStartupService extends ChannelStartupService { const key = { id: message.id, remoteJid, - fromMe: message.from === received.metadata.phone_number_id, + fromMe: this.isCloudApiFromMe(message, received), }; if (message.type === 'sticker') { From e4cd85ab24d1968ced65d2fb24fb4545a5b36fb6 Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 22:56:01 -0300 Subject: [PATCH 3/4] feat(meta): fallback pushName from persisted contact on cloud api --- .../channel/meta/whatsapp.business.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index c966f81a6..6b0d0cf08 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -434,6 +434,13 @@ export class BusinessStartupService extends ChannelStartupService { if (received.messages) { const message = received.messages[0]; // Añadir esta línea para definir message const remoteJid = createJid(message?.to ?? message?.from); + const contact = await this.prismaRepository.contact.findFirst({ + where: { instanceId: this.instanceId, remoteJid }, + }); + + if (!pushName) { + pushName = contact?.pushName ?? incomingContact?.user_id ?? incomingContact?.wa_id ?? undefined; + } const key = { id: message.id, @@ -740,10 +747,6 @@ export class BusinessStartupService extends ChannelStartupService { }); } - const contact = await this.prismaRepository.contact.findFirst({ - where: { instanceId: this.instanceId, remoteJid: key.remoteJid }, - }); - const contactPhone = incomingContact?.profile?.phone ?? incomingContact?.wa_id ?? message?.to ?? message?.from; if (!contactPhone) return; From 532bc1dc15964d38172a5983145cb9cfe97f22f8 Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 23:07:54 -0300 Subject: [PATCH 4/4] fix(meta): address review feedback on remoteId and status fromMe --- .../channel/meta/whatsapp.business.service.ts | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 6b0d0cf08..981c73ac2 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -127,15 +127,13 @@ export class BusinessStartupService extends ChannelStartupService { if (!data) return; const content = data.entry[0].changes[0].value; - const firstMessage = - content?.messages?.[0] ?? content?.message_echoes?.[0] ?? content?.smb_message_echoes?.[0] ?? undefined; - const recipient = content?.statuses?.[0]?.recipient_id; - const remoteId = firstMessage?.to ?? firstMessage?.from ?? recipient; + const normalizedContent = this.normalizeWebhookContent(content); + const remoteId = this.resolveRemoteId(normalizedContent); try { this.loadChatwoot(); - this.eventHandler(this.normalizeWebhookContent(content)); + this.eventHandler(normalizedContent); if (remoteId) { this.phoneNumber = createJid(remoteId); @@ -150,7 +148,9 @@ export class BusinessStartupService extends ChannelStartupService { if (!content || typeof content !== 'object') return content; const normalized = { ...content }; - const echoes = normalized?.message_echoes ?? normalized?.smb_message_echoes; + const messageEchoes = Array.isArray(normalized?.message_echoes) ? normalized.message_echoes : undefined; + const smbMessageEchoes = Array.isArray(normalized?.smb_message_echoes) ? normalized.smb_message_echoes : undefined; + const echoes = messageEchoes?.length ? messageEchoes : smbMessageEchoes?.length ? smbMessageEchoes : undefined; if (!Array.isArray(normalized.messages) && Array.isArray(echoes) && echoes.length > 0) { normalized.messages = echoes; @@ -163,6 +163,26 @@ export class BusinessStartupService extends ChannelStartupService { return typeof value === 'string' ? value.replace(/\D/g, '') : ''; } + private resolveRemoteId(content: any) { + const firstMessage = content?.messages?.[0]; + const recipient = content?.statuses?.[0]?.recipient_id; + + const candidates = [firstMessage?.from, firstMessage?.to, recipient].filter(Boolean) as string[]; + if (candidates.length === 0) return undefined; + + const businessNumbers = [ + this.normalizePhoneNumber(content?.metadata?.display_phone_number), + this.normalizePhoneNumber(content?.metadata?.phone_number_id), + ].filter(Boolean); + + const externalCounterpart = candidates.find((candidate) => { + const normalizedCandidate = this.normalizePhoneNumber(candidate); + return normalizedCandidate && !businessNumbers.includes(normalizedCandidate); + }); + + return externalCounterpart ?? candidates[0]; + } + private isCloudApiEchoPayload(received: any) { return Array.isArray(received?.message_echoes) || Array.isArray(received?.smb_message_echoes); } @@ -179,6 +199,16 @@ export class BusinessStartupService extends ChannelStartupService { return from === displayPhone || from === phoneNumberId; } + private isCloudApiStatusFromMe(item: any, received: any) { + const recipient = this.normalizePhoneNumber(item?.recipient_id); + if (!recipient) return true; + + const displayPhone = this.normalizePhoneNumber(received?.metadata?.display_phone_number); + const phoneNumberId = this.normalizePhoneNumber(received?.metadata?.phone_number_id); + + return recipient !== displayPhone && recipient !== phoneNumberId; + } + private async downloadMediaMessage(message: any) { try { const id = message[message.type].id; @@ -794,11 +824,13 @@ export class BusinessStartupService extends ChannelStartupService { } if (received.statuses) { for await (const item of received.statuses) { - const remoteJid = createJid(item?.recipient_id ?? this.phoneNumber); - const key = { + const remoteId = item?.recipient_id ?? this.phoneNumber; + if (!remoteId) continue; + + const key: any = { id: item.id, - remoteJid, - fromMe: item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true, + remoteJid: createJid(remoteId), + fromMe: this.isCloudApiStatusFromMe(item, received), }; if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { return; @@ -818,6 +850,14 @@ export class BusinessStartupService extends ChannelStartupService { return; } + const findMessageKey: any = findMessage?.key ?? {}; + if (findMessageKey?.remoteJid) { + key.remoteJid = findMessageKey.remoteJid; + } + if (typeof findMessageKey?.fromMe === 'boolean') { + key.fromMe = findMessageKey.fromMe; + } + if (item.message === null && item.status === undefined) { this.sendDataWebhook(Events.MESSAGES_DELETE, key);