diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3b612d98..4db8146c 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1251,10 +1251,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; @@ -1263,7 +1263,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 @@ -1343,14 +1343,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; } } @@ -1398,13 +1398,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; @@ -1480,7 +1481,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 }); } @@ -1502,7 +1503,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( @@ -1513,7 +1514,7 @@ export class BaileysStartupService extends ChannelStartupService { ); if (buffer) { - messageRaw.message.base64 = buffer.toString('base64'); + (messageRaw.message as any).base64 = buffer.toString('base64'); } } } catch (error) { @@ -1525,8 +1526,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); @@ -1534,7 +1535,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, }); @@ -1563,9 +1564,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, }, ]); } @@ -1611,7 +1614,18 @@ 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')) { + 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; + const normalizedParticipant = keyAny.participant; + + if (settings?.groupsIgnore && normalizedRemoteJid?.includes('@g.us')) { continue; } @@ -1662,9 +1676,9 @@ export class BaileysStartupService extends ChannelStartupService { const message: any = { keyId: key.id, - remoteJid: key?.remoteJid, + remoteJid: normalizedRemoteJid, fromMe: key.fromMe, - participant: key?.participant, + participant: normalizedParticipant, status: status[update.status] ?? 'SERVER_ACK', pollUpdates, instanceId: this.instanceId, @@ -1687,18 +1701,48 @@ 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; + const retryDelay = 500; // 500ms delay to avoid blocking for too long + + 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(retryDelay); + } + } 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; } + + // 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`, + ); + key.remoteJid = findMessage.key.remoteJid; + } message.messageId = findMessage.id; } @@ -2472,7 +2516,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 || @@ -2494,14 +2538,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)}`; } } @@ -4712,26 +4757,28 @@ 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 + 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.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', + messageType: getContentType(message.message), messageTimestamp: Long.isLong(message.messageTimestamp) ? message.messageTimestamp.toNumber() : (message.messageTimestamp as number), + source: getDevice(keyAny.id), instanceId: this.instanceId, - source: getDevice(message.key.id), + status: status[message.status], + contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo), }; if (!messageRaw.status && message.key.fromMe === false) { diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 08de0714..8d7a2c16 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'); @@ -164,9 +165,28 @@ 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 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.`, + ); + 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