Skip to content

Commit 6d696f3

Browse files
authored
fix(meta): handle message_echoes and guard missing contact fields (#2514)
* fix(meta): handle message_echoes and guard missing contact fields * feat(meta): fallback pushName from persisted contact on cloud api * fix(meta): address review feedback on remoteId and status fromMe
1 parent d6caf9b commit 6d696f3

1 file changed

Lines changed: 121 additions & 18 deletions

File tree

src/api/integrations/channel/meta/whatsapp.business.service.ts

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,99 @@ export class BusinessStartupService extends ChannelStartupService {
127127
if (!data) return;
128128

129129
const content = data.entry[0].changes[0].value;
130+
const normalizedContent = this.normalizeWebhookContent(content);
131+
const remoteId = this.resolveRemoteId(normalizedContent);
130132

131133
try {
132134
this.loadChatwoot();
133135

134-
const senderJid = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
135-
this.phoneNumber = senderJid;
136+
await this.eventHandler(normalizedContent);
136137

137-
await this.eventHandler(content, senderJid);
138+
if (remoteId) {
139+
this.phoneNumber = createJid(remoteId);
140+
}
138141
} catch (error) {
139142
this.logger.error(error);
140143
throw new InternalServerErrorException(error?.toString());
141144
}
142145
}
143146

147+
private normalizeWebhookContent(content: any) {
148+
if (!content || typeof content !== 'object') return content;
149+
150+
const normalized = { ...content };
151+
const messageEchoes = Array.isArray(normalized?.message_echoes) ? normalized.message_echoes : undefined;
152+
const smbMessageEchoes = Array.isArray(normalized?.smb_message_echoes) ? normalized.smb_message_echoes : undefined;
153+
const echoes = messageEchoes?.length ? messageEchoes : smbMessageEchoes?.length ? smbMessageEchoes : undefined;
154+
155+
if (!Array.isArray(normalized.messages) && Array.isArray(echoes) && echoes.length > 0) {
156+
normalized.messages = echoes;
157+
}
158+
159+
return normalized;
160+
}
161+
162+
private normalizePhoneNumber(value?: string) {
163+
return typeof value === 'string' ? value.replace(/\D/g, '') : '';
164+
}
165+
166+
private resolveRemoteId(content: any) {
167+
const firstMessage = content?.messages?.[0];
168+
const recipient = content?.statuses?.[0]?.recipient_id;
169+
170+
const candidates = [firstMessage?.from, firstMessage?.to, recipient].filter(Boolean) as string[];
171+
if (candidates.length === 0) return undefined;
172+
173+
const businessNumbers = [
174+
this.normalizePhoneNumber(content?.metadata?.display_phone_number),
175+
this.normalizePhoneNumber(content?.metadata?.phone_number_id),
176+
].filter(Boolean);
177+
178+
const externalCounterpart = candidates.find((candidate) => {
179+
const normalizedCandidate = this.normalizePhoneNumber(candidate);
180+
return normalizedCandidate && !businessNumbers.includes(normalizedCandidate);
181+
});
182+
183+
return externalCounterpart ?? candidates[0];
184+
}
185+
186+
private isCloudApiEchoPayload(received: any) {
187+
return (
188+
(Array.isArray(received?.message_echoes) && received.message_echoes.length > 0) ||
189+
(Array.isArray(received?.smb_message_echoes) && received.smb_message_echoes.length > 0)
190+
);
191+
}
192+
193+
private resolveMessageRemoteId(message: any, received: any) {
194+
if (this.isCloudApiEchoPayload(received)) {
195+
return message?.to ?? message?.from;
196+
}
197+
198+
return message?.from ?? message?.to;
199+
}
200+
201+
private isCloudApiFromMe(message: any, received: any) {
202+
if (this.isCloudApiEchoPayload(received)) return true;
203+
204+
const from = this.normalizePhoneNumber(message?.from);
205+
const displayPhone = this.normalizePhoneNumber(received?.metadata?.display_phone_number);
206+
const phoneNumberId = this.normalizePhoneNumber(received?.metadata?.phone_number_id);
207+
208+
if (!from) return false;
209+
210+
return from === displayPhone || from === phoneNumberId;
211+
}
212+
213+
private isCloudApiStatusFromMe(item: any, received: any) {
214+
const recipient = this.normalizePhoneNumber(item?.recipient_id);
215+
if (!recipient) return true;
216+
217+
const displayPhone = this.normalizePhoneNumber(received?.metadata?.display_phone_number);
218+
const phoneNumberId = this.normalizePhoneNumber(received?.metadata?.phone_number_id);
219+
220+
return recipient !== displayPhone && recipient !== phoneNumberId;
221+
}
222+
144223
private async downloadMediaMessage(message: any) {
145224
try {
146225
const id = message[message.type].id;
@@ -383,20 +462,34 @@ export class BusinessStartupService extends ChannelStartupService {
383462
return messageType;
384463
}
385464

386-
protected async messageHandle(received: any, database: Database, settings: any, senderJid: string) {
465+
protected async messageHandle(received: any, database: Database, settings: any) {
387466
try {
388467
let messageRaw: any;
389468
let pushName: any;
469+
const incomingContact = received?.contacts?.[0];
390470

391-
if (received.contacts) pushName = received.contacts[0].profile.name;
471+
if (incomingContact) {
472+
pushName = incomingContact?.profile?.name ?? incomingContact?.name ?? incomingContact?.wa_id ?? undefined;
473+
}
392474

393475
if (received.messages) {
394476
const message = received.messages[0];
477+
const remoteId = this.resolveMessageRemoteId(message, received);
478+
if (!remoteId) return;
479+
480+
const remoteJid = createJid(remoteId);
481+
const contact = await this.prismaRepository.contact.findFirst({
482+
where: { instanceId: this.instanceId, remoteJid },
483+
});
484+
485+
if (!pushName) {
486+
pushName = contact?.pushName ?? incomingContact?.user_id ?? incomingContact?.wa_id ?? undefined;
487+
}
395488

396489
const key = {
397490
id: message.id,
398-
remoteJid: senderJid,
399-
fromMe: message.from === received.metadata.phone_number_id,
491+
remoteJid,
492+
fromMe: this.isCloudApiFromMe(message, received),
400493
};
401494

402495
if (message.type === 'sticker') {
@@ -699,12 +792,11 @@ export class BusinessStartupService extends ChannelStartupService {
699792
});
700793
}
701794

702-
const contact = await this.prismaRepository.contact.findFirst({
703-
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
704-
});
795+
const contactPhone = incomingContact?.profile?.phone ?? incomingContact?.wa_id ?? remoteId;
796+
if (!contactPhone) return;
705797

706798
const contactRaw: any = {
707-
remoteJid: received.contacts[0].profile.phone,
799+
remoteJid: createJid(contactPhone),
708800
pushName,
709801
// profilePicUrl: '',
710802
instanceId: this.instanceId,
@@ -716,7 +808,7 @@ export class BusinessStartupService extends ChannelStartupService {
716808

717809
if (contact) {
718810
const contactRaw: any = {
719-
remoteJid: received.contacts[0].profile.phone,
811+
remoteJid: createJid(contactPhone),
720812
pushName,
721813
// profilePicUrl: '',
722814
instanceId: this.instanceId,
@@ -747,10 +839,13 @@ export class BusinessStartupService extends ChannelStartupService {
747839
}
748840
if (received.statuses) {
749841
for await (const item of received.statuses) {
750-
const key = {
842+
const remoteId = item?.recipient_id ?? this.phoneNumber;
843+
if (!remoteId) continue;
844+
845+
const key: any = {
751846
id: item.id,
752-
remoteJid: senderJid,
753-
fromMe: senderJid === received.metadata.phone_number_id,
847+
remoteJid: createJid(remoteId),
848+
fromMe: this.isCloudApiStatusFromMe(item, received),
754849
};
755850
if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) {
756851
return;
@@ -770,6 +865,14 @@ export class BusinessStartupService extends ChannelStartupService {
770865
return;
771866
}
772867

868+
const findMessageKey: any = findMessage?.key ?? {};
869+
if (findMessageKey?.remoteJid) {
870+
key.remoteJid = findMessageKey.remoteJid;
871+
}
872+
if (typeof findMessageKey?.fromMe === 'boolean') {
873+
key.fromMe = findMessageKey.fromMe;
874+
}
875+
773876
if (item.message === null && item.status === undefined) {
774877
this.sendDataWebhook(Events.MESSAGES_DELETE, key);
775878

@@ -895,7 +998,7 @@ export class BusinessStartupService extends ChannelStartupService {
895998
return message;
896999
}
8971000

898-
protected async eventHandler(content: any, senderJid: string) {
1001+
protected async eventHandler(content: any) {
8991002
try {
9001003
this.logger.log('Contenido recibido en eventHandler:');
9011004
this.logger.log(JSON.stringify(content, null, 2));
@@ -920,12 +1023,12 @@ export class BusinessStartupService extends ChannelStartupService {
9201023
message.type === 'button' ||
9211024
message.type === 'reaction'
9221025
) {
923-
await this.messageHandle(content, database, settings, senderJid);
1026+
await this.messageHandle(content, database, settings);
9241027
} else {
9251028
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
9261029
}
9271030
} else if (content.statuses) {
928-
await this.messageHandle(content, database, settings, senderJid);
1031+
await this.messageHandle(content, database, settings);
9291032
} else {
9301033
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
9311034
}

0 commit comments

Comments
 (0)