From b43961db07a160cefb9ca362472766423467185a Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 19:13:13 -0300 Subject: [PATCH 1/3] fix(meta): handle message_echoes and guard missing contact fields --- .../channel/meta/whatsapp.business.service.ts | 118 +++++++++++++++--- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 03c8eb44c..ba09a3648 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -127,20 +127,99 @@ export class BusinessStartupService extends ChannelStartupService { if (!data) return; const content = data.entry[0].changes[0].value; + const normalizedContent = this.normalizeWebhookContent(content); + const remoteId = this.resolveRemoteId(normalizedContent); try { this.loadChatwoot(); - const senderJid = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id); - this.phoneNumber = senderJid; + await this.eventHandler(normalizedContent); - await this.eventHandler(content, senderJid); + 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 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; + } + + return normalized; + } + + private normalizePhoneNumber(value?: string) { + 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) && received.message_echoes.length > 0) || + (Array.isArray(received?.smb_message_echoes) && received.smb_message_echoes.length > 0) + ); + } + + private resolveMessageRemoteId(message: any, received: any) { + if (this.isCloudApiEchoPayload(received)) { + return message?.to ?? message?.from; + } + + return message?.from ?? message?.to; + } + + 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 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; @@ -383,20 +462,27 @@ export class BusinessStartupService extends ChannelStartupService { return messageType; } - protected async messageHandle(received: any, database: Database, settings: any, senderJid: string) { + protected async messageHandle(received: any, database: Database, settings: any) { 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]; + const remoteId = this.resolveMessageRemoteId(message, received); + if (!remoteId) return; + + const remoteJid = createJid(remoteId); const key = { id: message.id, - remoteJid: senderJid, - fromMe: message.from === received.metadata.phone_number_id, + remoteJid, + fromMe: this.isCloudApiFromMe(message, received), }; if (message.type === 'sticker') { @@ -703,8 +789,11 @@ export class BusinessStartupService extends ChannelStartupService { where: { instanceId: this.instanceId, remoteJid: key.remoteJid }, }); + const contactPhone = incomingContact?.profile?.phone ?? incomingContact?.wa_id ?? remoteId; + if (!contactPhone) return; + const contactRaw: any = { - remoteJid: received.contacts[0].profile.phone, + remoteJid: createJid(contactPhone), pushName, // profilePicUrl: '', instanceId: this.instanceId, @@ -716,7 +805,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, @@ -747,10 +836,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: senderJid, - fromMe: senderJid === received.metadata.phone_number_id, + remoteJid, + fromMe: this.isCloudApiStatusFromMe(item, received), }; if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { return; @@ -895,7 +985,7 @@ export class BusinessStartupService extends ChannelStartupService { return message; } - protected async eventHandler(content: any, senderJid: string) { + protected async eventHandler(content: any) { try { this.logger.log('Contenido recibido en eventHandler:'); this.logger.log(JSON.stringify(content, null, 2)); @@ -920,12 +1010,12 @@ export class BusinessStartupService extends ChannelStartupService { message.type === 'button' || message.type === 'reaction' ) { - await this.messageHandle(content, database, settings, senderJid); + await this.messageHandle(content, database, settings); } else { this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`); } } else if (content.statuses) { - await this.messageHandle(content, database, settings, senderJid); + await this.messageHandle(content, database, settings); } else { this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido'); } From 56dceb9020b11121aadaeca99e90e3208a307443 Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 22:56:01 -0300 Subject: [PATCH 2/3] 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 ba09a3648..f4e9bc99b 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -478,6 +478,13 @@ export class BusinessStartupService extends ChannelStartupService { if (!remoteId) return; const remoteJid = createJid(remoteId); + 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, @@ -785,10 +792,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 ?? remoteId; if (!contactPhone) return; From efbedab6ac5627f5372e8ba54d49dfb9ae5391da Mon Sep 17 00:00:00 2001 From: Ricardo Henrique Date: Fri, 24 Apr 2026 23:07:54 -0300 Subject: [PATCH 3/3] fix(meta): address review feedback on remoteId and status fromMe --- .../channel/meta/whatsapp.business.service.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index f4e9bc99b..d08fbe259 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -839,10 +839,12 @@ 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, + remoteJid: createJid(remoteId), fromMe: this.isCloudApiStatusFromMe(item, received), }; if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { @@ -863,6 +865,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);