diff --git a/Dockerfile b/Dockerfile index 1c9759cb..d30ccf0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM node:20-alpine AS builder RUN apk update && \ apk add git ffmpeg wget curl bash openssl -LABEL version="2.2.3.17" description="Api to control whatsapp features through http requests." +LABEL version="2.2.3.22" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@atendai.com" diff --git a/package-lock.json b/package-lock.json index daa0418c..5e4a4ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "evolution-api", - "version": "2.2.3.17", + "version": "2.2.3.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "evolution-api", - "version": "2.2.3.17", + "version": "2.2.3.22", "license": "Apache-2.0", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", diff --git a/package.json b/package.json index ebd58b28..c81d00ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "2.2.3.17", + "version": "2.2.3.22", "description": "Rest api for communication with WhatsApp", "main": "./dist/main.js", "type": "commonjs", diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 207d8ba5..5b715924 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -19,11 +19,43 @@ import { Query } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; import { Contact, Message, MessageUpdate } from '@prisma/client'; +class SimpleMutex { + private locked = false; + private waiting: Array<() => void> = []; + + async acquire(): Promise { + if (this.locked) { + await new Promise(resolve => this.waiting.push(resolve)); + } + this.locked = true; + } + + release(): void { + const next = this.waiting.shift(); + if (next) next(); + else this.locked = false; + } + + async runExclusive(fn: () => Promise): Promise { + await this.acquire(); + try { + return await fn(); + } finally { + this.release(); + } + } +} + + export class ChatController { constructor(private readonly waMonitor: WAMonitoringService) {} + private static whatsappNumberMutex = new SimpleMutex(); + public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { - return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); + return await ChatController.whatsappNumberMutex.runExclusive(async () => { + return this.waMonitor.waInstances[instanceName].whatsappNumber(data); + }); } public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 0a56305f..c1026a84 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -136,7 +136,7 @@ import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; import cron from 'node-cron'; import { release } from 'os'; -import { join } from 'path'; +import path, { join } from 'path'; import P from 'pino'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; @@ -1296,13 +1296,25 @@ export class BaileysStartupService extends ChannelStartupService { true, ); - const { buffer, mediaType, fileName, size } = media; - const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName); + const { buffer, mediaType, fileName: originalName, size } = media; + const mimetype = mimeTypes.lookup(originalName).toString(); + + // calcula a extensão (usa a do nome original ou, em último caso, a do mimetype) + const ext = path.extname(originalName) || `.${mimeTypes.extension(mimetype)}`; + + // força usar sempre o id da mensagem como nome de arquivo + const fileName = `${received.key.id}${ext}`; + + const fullName = join( + this.instance.id, + received.key.remoteJid, + mediaType, + fileName, + ); + await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype, }); - await this.prismaRepository.media.create({ data: { messageId: msg.id, @@ -1428,6 +1440,11 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + if (!key.id) { + console.warn(`Mensagem sem key.id, pulando update: ${JSON.stringify(key)}`); + continue; + } + if (status[update.status] === 'READ' && key.fromMe) { if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { this.chatwootService.eventWhatsapp( @@ -1515,7 +1532,7 @@ export class BaileysStartupService extends ChannelStartupService { remoteJid: key.remoteJid, fromMe: key.fromMe, participant: key?.remoteJid, - status: status[update.status], + status: status[update.status]?? 'UNKNOWN', pollUpdates, instanceId: this.instanceId, }; @@ -4476,29 +4493,41 @@ export class BaileysStartupService extends ChannelStartupService { return unreadMessages; } - private async addLabel(labelId: string, instanceId: string, chatId: string) { - const id = cuid(); - - await this.prismaRepository.$executeRawUnsafe( - `INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") - VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") - DO - UPDATE - SET "labels" = ( - SELECT to_jsonb(array_agg(DISTINCT elem)) - FROM ( - SELECT jsonb_array_elements_text("Chat"."labels") AS elem - UNION - SELECT $1::text AS elem - ) sub - ), - "updatedAt" = NOW();`, - labelId, - instanceId, - chatId, - id, - ); + private async addLabel( + labelId: string, + instanceId: string, + chatId: string + ): Promise { + try { + await this.prismaRepository.$executeRawUnsafe( + `UPDATE "Chat" + SET "labels" = ( + SELECT to_jsonb(array_agg(DISTINCT elem)) + FROM ( + SELECT jsonb_array_elements_text("Chat".labels) AS elem + UNION + SELECT $1::text AS elem + ) sub + ), + "updatedAt" = NOW() + WHERE "instanceId" = $2 + AND "remoteJid" = $3;`, + labelId, + instanceId, + chatId + ); + } catch (err: unknown) { + // Não deixa quebrar nada: registra e segue em frente + const msg = + err instanceof Error ? err.message : JSON.stringify(err); + // Use console.warn para evitar conflito de assinatura de método + console.warn( + `Failed to add label ${labelId} to chat ${chatId}@${instanceId}: ${msg}` + ); + } } + + private async removeLabel(labelId: string, instanceId: string, chatId: string) { const id = cuid(); diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index db863ead..c2fe0fd8 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -2171,27 +2171,47 @@ export class ChatwootService { } if (body.key.remoteJid.includes('@g.us')) { - const participantName = body.pushName; - const rawPhoneNumber = body.key.participant.split('@')[0]; - const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/); - - let formattedPhoneNumber: string; - - if (phoneMatch) { - formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`; - } else { - formattedPhoneNumber = `+${rawPhoneNumber}`; + // Extrai de forma segura o JID do participante + const participantJid = body.key.participant; + + // Se não veio participant, envia mensagem crua + if (!participantJid) { + const rawContent = bodyMessage; + const sent = await this.createMessage( + instance, + getConversation, + rawContent, + messageType, + false, + [], + body, + 'WAID:' + body.key.id, + quotedMsg, + ); + if (!sent) this.logger.warn('message not sent'); + return sent; } - - let content: string; - - if (!body.key.fromMe) { - content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`; - } else { - content = `${bodyMessage}`; - } - - const send = await this.createMessage( + + // Formata o telefone + const rawPhone = participantJid.split('@')[0]; + const match = rawPhone.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/); + const formattedPhone = match + ? `+${match[1]} (${match[2]}) ${match[3]}-${match[4]}` + : `+${rawPhone}`; + + // Define prefixo com número e nome (ou só número, se pushName vazio) + const name = body.pushName?.trim(); + const prefix = name + ? `**${formattedPhone} – ${name}:**\n\n` + : `**${formattedPhone}:**\n\n`; + + // Monta o conteúdo, omitindo prefixo em mensagens enviadas por mim + const content = body.key.fromMe + ? bodyMessage + : `${prefix}${bodyMessage}`; + + // Envia a mensagem formatada + const sent = await this.createMessage( instance, getConversation, content, @@ -2202,13 +2222,8 @@ export class ChatwootService { 'WAID:' + body.key.id, quotedMsg, ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - return send; + if (!sent) this.logger.warn('message not sent'); + return sent; } else { const send = await this.createMessage( instance, diff --git a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts b/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts index 408b20b8..cff97a5b 100644 --- a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts +++ b/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts @@ -6,6 +6,7 @@ import { Chatwoot, configService } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { inbox } from '@figuro/chatwoot-sdk'; import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client'; +import axios from 'axios'; import { proto } from 'baileys'; type ChatwootUser = { @@ -209,6 +210,7 @@ class ChatwootImport { throw new Error('User not found to import messages.'); } + const touchedConversations = new Set(); let totalMessagesImported = 0; let messagesOrdered = this.historyMessages.get(instance.instanceName) || []; @@ -284,6 +286,11 @@ class ChatwootImport { phoneNumbersWithTimestamp, messagesByPhoneNumber, ); + + for (const { conversation_id } of fksByNumber.values()) { + touchedConversations.add(conversation_id); + } + this.logger.info( `[importHistoryMessages] Batch ${batchNumber}: FKs recuperados para ${fksByNumber.size} números.` ); @@ -336,6 +343,8 @@ class ChatwootImport { ${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`; }); }); + + if (bindInsertMsg.length > 2) { if (sqlInsertMsg.slice(-1) === ',') { sqlInsertMsg = sqlInsertMsg.slice(0, -1); @@ -354,10 +363,24 @@ class ChatwootImport { this.deleteHistoryMessages(instance); this.deleteRepositoryMessagesCache(instance); + + + this.logger.info( `[importHistoryMessages] Histórico e cache de mensagens da instância "${instance.instanceName}" foram limpos.` ); + + for (const convId of touchedConversations) { + await this.safeRefreshConversation( + provider.url, + provider.accountId, + convId, + provider.token + ); + } + + const providerData: ChatwootDto = { ...provider, ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [], @@ -719,6 +742,42 @@ class ChatwootImport { return pgClient.query(sql, [`WAID:${sourceId}`, messageId]); } + + private async safeRefreshConversation( + providerUrl: string, + accountId: string, + conversationId: string, + apiToken: string + ): Promise { + try { + const pgClient = postgresClient.getChatwootConnection(); + const res = await pgClient.query( + `SELECT display_id + FROM conversations + WHERE id = $1 + LIMIT 1`, + [parseInt(conversationId, 10)] + ); + const displayId = res.rows[0]?.display_id as string; + if (!displayId) { + this.logger.warn(`Conversation ${conversationId} sem display_id.`); + return; + } + + const url = `${providerUrl}/api/v1/accounts/${accountId}/conversations/${displayId}/refresh`; + await axios.post(url, null, { + params: { api_access_token: apiToken }, + }); + this.logger.verbose(`Conversa ${displayId} refreshada com sucesso.`); + } catch (err: any) { + this.logger.warn( + `Não foi possível dar refresh na conversa ${conversationId}: ${err.message}` + ); + } + } + + + } export const chatwootImport = new ChatwootImport();