diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index aec1806fb..5bb440007 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -88,8 +88,8 @@ import { sendTelemetry } from '@utils/sendTelemetry'; 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 axios from 'axios'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -442,7 +442,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, ), ); @@ -468,16 +468,16 @@ export class BaileysStartupService extends ChannelStartupService { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; - + // 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...'); return; } - + const shouldReconnect = !codesToNotReconnect.includes(statusCode); this.logger.info({ @@ -494,9 +494,7 @@ export class BaileysStartupService extends ChannelStartupService { await this.connectToWhatsapp(this.phoneNumber); }, 3000); } else { - this.logger.info( - `Skipping reconnection for status code ${statusCode} (code is in codesToNotReconnect list)`, - ); + 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', @@ -1019,13 +1017,15 @@ 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) { + const normalizedProgress = progress ?? -1; + + if (normalizedProgress <= this.historySyncLastProgress) { this.historySyncMessageCount = 0; this.historySyncChatCount = 0; this.historySyncContactCount = 0; } - this.historySyncLastProgress = progress ?? -1; + + this.historySyncLastProgress = normalizedProgress; if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) { console.log('received on-demand history sync, messages=', messages); @@ -1129,16 +1129,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) { @@ -1202,15 +1202,12 @@ export class BaileysStartupService extends ChannelStartupService { const filteredContacts = contacts.filter((c) => !!c.notify || !!c.name); this.historySyncContactCount += filteredContacts.length; - await this.contactHandle['contacts.upsert']( - filteredContacts.map((c) => ({ id: c.id, name: c.name ?? c.notify })), - ); - - if (progress === 100) { + if (normalizedProgress === 100) { this.sendDataWebhook(Events.MESSAGING_HISTORY_SET, { messageCount: this.historySyncMessageCount, chatCount: this.historySyncChatCount, contactCount: this.historySyncContactCount, + progress: normalizedProgress, }); this.historySyncMessageCount = 0; @@ -1219,6 +1216,10 @@ export class BaileysStartupService extends ChannelStartupService { this.historySyncLastProgress = -1; } + await this.contactHandle['contacts.upsert']( + filteredContacts.map((c) => ({ id: c.id, name: c.name ?? c.notify })), + ); + contacts = undefined; messages = undefined; chats = undefined; @@ -1826,7 +1827,7 @@ export class BaileysStartupService extends ChannelStartupService { if (!findMessage?.id) { 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)}`, + `Original message not found for update. Skipping. This is expected for protocol messages or ephemeral events not saved to the database. Key: ${JSON.stringify(key)}`, ); continue; } @@ -2337,12 +2338,12 @@ export class BaileysStartupService extends ChannelStartupService { const url = match[0].replace(/[.,);\]]+$/u, ''); if (!url) return undefined; - const previewData = await getLinkPreview(url, { + 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; + })) as any; if (!previewData || !previewData.title) return undefined; @@ -2356,9 +2357,9 @@ export class BaileysStartupService extends ChannelStartupService { thumbnailUrl: image, sourceUrl: url, mediaUrl: url, - renderLargerThumbnail: true + renderLargerThumbnail: true, // showAdAttribution: true // Removed to prevent "Sent via ad" label - } + }, }; } catch (error) { this.logger.error(`Error generating link preview: ${error}`); diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5c2e03891..42fdabd63 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -641,16 +641,16 @@ export class ChatwootService { const isLid = body.key.addressingMode === 'lid'; const isGroup = body.key.remoteJid.endsWith('@g.us'); let phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid; - let { remoteJid } = body.key; + const { 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); @@ -974,9 +974,7 @@ 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) - ); + 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 = { @@ -1132,9 +1130,7 @@ export class ChatwootService { const replyToIds = await this.getReplyToIds(messageBody, instance); // Filtra valores null/undefined antes de enviar - const filteredReplyToIds = Object.fromEntries( - Object.entries(replyToIds).filter(([_, value]) => value != null) - ); + const filteredReplyToIds = Object.fromEntries(Object.entries(replyToIds).filter(([, value]) => value != null)); if (Object.keys(filteredReplyToIds).length > 0) { const contentAttrs = JSON.stringify(filteredReplyToIds); @@ -1841,32 +1837,32 @@ 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, - orderMessage: msg.orderMessage, - quotedProductMessage: msg.contextInfo?.quotedMessage?.productMessage, - 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, + orderMessage: msg.orderMessage, + quotedProductMessage: msg.contextInfo?.quotedMessage?.productMessage, + 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); @@ -1894,39 +1890,39 @@ 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; - } + if (typeKey === 'quotedProductMessage' && result?.product) { + const product = result.product; - 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?'}"_` - ); -} + // 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; @@ -2166,11 +2162,11 @@ if (typeKey === 'quotedProductMessage' && result?.product) { 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); @@ -2748,13 +2744,13 @@ if (typeKey === 'quotedProductMessage' && result?.product) { 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}`); } @@ -2764,7 +2760,7 @@ if (typeKey === 'quotedProductMessage' && result?.product) { */ 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; @@ -2788,7 +2784,7 @@ if (typeKey === 'quotedProductMessage' && result?.product) { 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';