diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 92531b7e..e2b7d185 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -59,7 +59,7 @@ body: value: | - OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0] - Node.js version: [e.g. 18.17.0] - - Evolution API version: [e.g. 2.3.4] + - Evolution API version: [e.g. 2.3.5] - Database: [e.g. PostgreSQL 14, MySQL 8.0] - Connection type: [e.g. Baileys, WhatsApp Business API] validations: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3b92f9..9dfb834c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 2.3.5 (develop) + +### Fixed + +* **Kafka Migration**: Fixed PostgreSQL migration error for Kafka integration + - Corrected table reference from `"public"."Instance"` to `"Instance"` in foreign key constraint + - Fixed `ERROR: relation "public.Instance" does not exist` issue in migration `20250918182355_add_kafka_integration` + - Aligned table naming convention with other Evolution API migrations for consistency + - Resolved database migration failure that prevented Kafka integration setup +* **Update Baileys Version**: v7.0.0-rc.4 +* Refactor connection with PostgreSQL and improve message handling + + +### + # 2.3.4 (2025-09-23) ### Features diff --git a/Docker/kafka/docker-compose.yaml b/Docker/kafka/docker-compose.yaml new file mode 100644 index 00000000..dc1c794e --- /dev/null +++ b/Docker/kafka/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.3' + +services: + zookeeper: + container_name: zookeeper + image: confluentinc/cp-zookeeper:7.5.0 + environment: + - ZOOKEEPER_CLIENT_PORT=2181 + - ZOOKEEPER_TICK_TIME=2000 + - ZOOKEEPER_SYNC_LIMIT=2 + volumes: + - zookeeper_data:/var/lib/zookeeper/ + ports: + - 2181:2181 + + kafka: + container_name: kafka + image: confluentinc/cp-kafka:7.5.0 + depends_on: + - zookeeper + environment: + - KAFKA_BROKER_ID=1 + - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 + - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,OUTSIDE:PLAINTEXT + - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,OUTSIDE://host.docker.internal:9094 + - KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT + - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 + - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 + - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 + - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true + - KAFKA_LOG_RETENTION_HOURS=168 + - KAFKA_LOG_SEGMENT_BYTES=1073741824 + - KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS=300000 + - KAFKA_COMPRESSION_TYPE=gzip + ports: + - 29092:29092 + - 9092:9092 + - 9094:9094 + volumes: + - kafka_data:/var/lib/kafka/data + +volumes: + zookeeper_data: + kafka_data: + + +networks: + evolution-net: + name: evolution-net + driver: bridge \ No newline at end of file diff --git a/Docker/swarm/evolution_api_v2.yaml b/Docker/swarm/evolution_api_v2.yaml index 4055dfa3..f28163cc 100644 --- a/Docker/swarm/evolution_api_v2.yaml +++ b/Docker/swarm/evolution_api_v2.yaml @@ -2,7 +2,7 @@ version: "3.7" services: evolution_v2: - image: evoapicloud/evolution-api:v2.3.1 + image: evoapicloud/evolution-api:v2.3.5 volumes: - evolution_instances:/evolution/instances networks: diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 2ca3424e..35868ab7 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -15,6 +15,16 @@ services: expose: - 8080 + frontend: + container_name: evolution_frontend + image: evolution/manager:local + build: ./evolution-manager-v2 + restart: always + ports: + - "3000:80" + networks: + - evolution-net + volumes: evolution_instances: diff --git a/docker-compose.yaml b/docker-compose.yaml index b049f00f..e0edee65 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,15 @@ services: expose: - "8080" + frontend: + container_name: evolution_frontend + image: evoapicloud/evolution-manager:latest + restart: always + ports: + - "3000:80" + networks: + - evolution-net + redis: container_name: evolution_redis image: redis:latest diff --git a/evolution-manager-v2 b/evolution-manager-v2 index fcb38dd4..e510b5f1 160000 --- a/evolution-manager-v2 +++ b/evolution-manager-v2 @@ -1 +1 @@ -Subproject commit fcb38dd407b89697b7a7154cfd873f76729e6ece +Subproject commit e510b5f17fc75cbddeaaba102ddd568e4b127455 diff --git a/package-lock.json b/package-lock.json index 5b558ca0..815b9775 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "evolution-api", - "version": "2.3.4", + "version": "2.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "evolution-api", - "version": "2.3.4", + "version": "2.3.5", "license": "Apache-2.0", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", @@ -21,7 +21,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc.3", + "baileys": "^7.0.0-rc.5", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -5993,19 +5993,19 @@ } }, "node_modules/baileys": { - "version": "7.0.0-rc.3", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.3.tgz", - "integrity": "sha512-SVRLTDMKnWhX2P+bANVO1s/IigmGsN2UMbR69ftwsj+DtHxmSn8qjWftVZ//dTOhkncU7xZhTEOWtsOrPoemvQ==", + "version": "7.0.0-rc.5", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.5.tgz", + "integrity": "sha512-y95gW7UtKbD4dQb46G75rnr0U0LtnBItA002ARggDiCgm92Z8wnM+wxqC8OI/sDFanz3TgzqE4t7MPwNusUqUQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", - "axios": "^1.6.0", "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", "lru-cache": "^11.1.0", "music-metadata": "^11.7.0", + "p-queue": "^9.0.0", "pino": "^9.6", "protobufjs": "^7.2.4", "ws": "^8.13.0" @@ -12169,6 +12169,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.0.0.tgz", + "integrity": "sha512-KO1RyxstL9g1mK76530TExamZC/S2Glm080Nx8PE5sTd7nlduDQsAfEl4uXX+qZjLiwvDauvzXavufy3+rJ9zQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.0.tgz", + "integrity": "sha512-DhZ7ydOE3JXtXzDf2wz/KEamkKAD7Il5So09I2tOz4i+9pLcdghDKKmODkkoHKJ0TyczmhcHNxyTeTrsENT81A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", diff --git a/package.json b/package.json index 748439d6..bff453fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "2.3.4", + "version": "2.3.5", "description": "Rest api for communication with WhatsApp", "main": "./dist/main.js", "type": "commonjs", @@ -77,7 +77,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc.3", + "baileys": "^7.0.0-rc.5", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", diff --git a/prisma/postgresql-migrations/20250918182355_add_kafka_integration/migration.sql b/prisma/postgresql-migrations/20250918182355_add_kafka_integration/migration.sql index 16ef2d7c..2985c550 100644 --- a/prisma/postgresql-migrations/20250918182355_add_kafka_integration/migration.sql +++ b/prisma/postgresql-migrations/20250918182355_add_kafka_integration/migration.sql @@ -1,5 +1,5 @@ -- CreateTable -CREATE TABLE "public"."Kafka" ( +CREATE TABLE "Kafka" ( "id" TEXT NOT NULL, "enabled" BOOLEAN NOT NULL DEFAULT false, "events" JSONB NOT NULL, @@ -11,7 +11,7 @@ CREATE TABLE "public"."Kafka" ( ); -- CreateIndex -CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "public"."Kafka"("instanceId"); +CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "Kafka"("instanceId"); -- AddForeignKey -ALTER TABLE "public"."Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts b/src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts index 951be1a0..cb667f9c 100644 --- a/src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts +++ b/src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts @@ -71,7 +71,7 @@ export const useVoiceCallsBaileys = async ( socket.on('assertSessions', async (jids, force, callback) => { try { - const response = await baileys_sock.assertSessions(jids, force); + const response = await baileys_sock.assertSessions(jids); callback(response); diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index a3f70062..3d463079 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1512,7 +1512,11 @@ export class BaileysStartupService extends ChannelStartupService { `) as any[]; findMessage = messages[0] || null; - if (findMessage) message.messageId = findMessage.id; + if (!findMessage?.id) { + this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); + continue; + } + message.messageId = findMessage.id; } if (update.message === null && update.status === undefined) { @@ -3395,18 +3399,18 @@ export class BaileysStartupService extends ChannelStartupService { } const numberJid = numberVerified?.jid || user.jid; - const lid = - typeof numberVerified?.lid === 'string' - ? numberVerified.lid - : numberJid.includes('@lid') - ? numberJid.split('@')[1] - : undefined; + // const lid = + // typeof numberVerified?.lid === 'string' + // ? numberVerified.lid + // : numberJid.includes('@lid') + // ? numberJid.split('@')[1] + // : undefined; return new OnWhatsAppDto( numberJid, !!numberVerified?.exists, user.number, contacts.find((c) => c.remoteJid === numberJid)?.pushName, - lid, + // lid, ); }), ); @@ -4589,8 +4593,8 @@ export class BaileysStartupService extends ChannelStartupService { return response; } - public async baileysAssertSessions(jids: string[], force: boolean) { - const response = await this.client.assertSessions(jids, force); + public async baileysAssertSessions(jids: string[]) { + const response = await this.client.assertSessions(jids); return response; } diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index e5a0bc07..f1e217f5 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -33,6 +33,8 @@ import mimeTypes from 'mime-types'; import path from 'path'; import { Readable } from 'stream'; +const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds + interface ChatwootMessage { messageId?: number; inboxId?: number; @@ -72,7 +74,9 @@ export class ChatwootService { private readonly cache: CacheService, ) {} - private pgClient = postgresClient.getChatwootConnection(); + private async getPgClient() { + return postgresClient.getChatwootConnection(); + } private async getProvider(instance: InstanceDto): Promise { const cacheKey = `${instance.instanceName}:getProvider`; @@ -401,7 +405,8 @@ export class ChatwootService { if (!uri) return false; const sqlTags = `SELECT id, taggings_count FROM tags WHERE name = $1 LIMIT 1`; - const tagData = (await this.pgClient.query(sqlTags, [nameInbox]))?.rows[0]; + const pgClient = await this.getPgClient(); + const tagData = (await pgClient.query(sqlTags, [nameInbox]))?.rows[0]; let tagId = tagData?.id; const taggingsCount = tagData?.taggings_count || 0; @@ -411,18 +416,18 @@ export class ChatwootService { DO UPDATE SET taggings_count = tags.taggings_count + 1 RETURNING id`; - tagId = (await this.pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id; + tagId = (await pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id; const sqlCheckTagging = `SELECT 1 FROM taggings WHERE tag_id = $1 AND taggable_type = 'Contact' AND taggable_id = $2 AND context = 'labels' LIMIT 1`; - const taggingExists = (await this.pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0; + const taggingExists = (await pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0; if (!taggingExists) { const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at) VALUES ($1, 'Contact', $2, 'labels', NOW())`; - await this.pgClient.query(sqlInsertLabel, [tagId, contactId]); + await pgClient.query(sqlInsertLabel, [tagId, contactId]); } return true; @@ -620,12 +625,7 @@ export class ChatwootService { this.logger.verbose(`--- Start createConversation ---`); this.logger.verbose(`Instance: ${JSON.stringify(instance)}`); - // If it already exists in the cache, return conversationId - if (await this.cache.has(cacheKey)) { - const conversationId = (await this.cache.get(cacheKey)) as number; - this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`); - return conversationId; - } + // Always check Chatwoot first, cache only as fallback // If lock already exists, wait until release or timeout if (await this.cache.has(lockKey)) { @@ -651,12 +651,9 @@ export class ChatwootService { try { /* - Double check after lock - Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock + Double check after lock - REMOVED + This was causing the system to use cached conversations instead of checking Chatwoot */ - if (await this.cache.has(cacheKey)) { - return (await this.cache.get(cacheKey)) as number; - } const client = await this.clientCw(instance); if (!client) return null; @@ -763,34 +760,39 @@ export class ChatwootService { return null; } - let inboxConversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); - if (inboxConversation) { - if (this.provider.reopenConversation) { - this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`); - if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') { - await client.conversations.toggleStatus({ - accountId: this.provider.accountId, - conversationId: inboxConversation.id, - data: { - status: 'pending', - }, - }); - } - } else { - inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); - } + let inboxConversation = null; + + if (this.provider.reopenConversation) { + inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); if (inboxConversation) { - this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`); - this.cache.set(cacheKey, inboxConversation.id); - return inboxConversation.id; + this.logger.verbose( + `Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`, + ); + } else { + inboxConversation = await this.findAndReopenResolvedConversation( + client, + contactConversations.payload, + filterInbox.id, + ); } + } else { + inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); + } + + if (inboxConversation) { + this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`); + this.cache.set(cacheKey, inboxConversation.id); + return inboxConversation.id; + } + + if (await this.cache.has(cacheKey)) { + const conversationId = (await this.cache.get(cacheKey)) as number; + this.logger.warn( + `No active conversations found in Chatwoot, using cached conversation ID: ${conversationId} as fallback`, + ); + return conversationId; } const data = { @@ -833,6 +835,45 @@ export class ChatwootService { } } + private findOpenConversation(conversations: any[], inboxId: number): any | null { + const openConversation = conversations.find( + (conversation) => conversation && conversation.status !== 'resolved' && conversation.inbox_id == inboxId, + ); + + if (openConversation) { + this.logger.verbose(`Found open conversation: ${JSON.stringify(openConversation)}`); + } + + return openConversation || null; + } + + private async findAndReopenResolvedConversation( + client: any, + conversations: any[], + inboxId: number, + ): Promise { + const resolvedConversation = conversations.find( + (conversation) => conversation && conversation.status === 'resolved' && conversation.inbox_id == inboxId, + ); + + if (resolvedConversation) { + this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(resolvedConversation)}`); + if (this.provider.conversationPending && resolvedConversation.status !== 'open') { + await client.conversations.toggleStatus({ + accountId: this.provider.accountId, + conversationId: resolvedConversation.id, + data: { + status: 'pending', + }, + }); + this.logger.verbose(`Reopened resolved conversation ID: ${resolvedConversation.id}`); + } + return resolvedConversation; + } + + return null; + } + public async getInbox(instance: InstanceDto): Promise { const cacheKey = `${instance.instanceName}:getInbox`; if (await this.cache.has(cacheKey)) { @@ -880,6 +921,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + messageBodyForRetry?: any, ) { const client = await this.clientCw(instance); @@ -888,32 +930,86 @@ export class ChatwootService { return null; } - const replyToIds = await this.getReplyToIds(messageBody, instance); + const doCreateMessage = async (convId: number) => { + const replyToIds = await this.getReplyToIds(messageBody, instance); - const sourceReplyId = quotedMsg?.chatwootMessageId || null; + const sourceReplyId = quotedMsg?.chatwootMessageId || null; - const message = await client.messages.create({ - accountId: this.provider.accountId, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - source_id: sourceId, - content_attributes: { - ...replyToIds, + const message = await client.messages.create({ + accountId: this.provider.accountId, + conversationId: convId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + source_id: sourceId, + content_attributes: { + ...replyToIds, + }, + source_reply_id: sourceReplyId ? sourceReplyId.toString() : null, }, - source_reply_id: sourceReplyId ? sourceReplyId.toString() : null, - }, - }); + }); - if (!message) { - this.logger.warn('message not found'); - return null; + if (!message) { + this.logger.warn('message not found'); + return null; + } + + return message; + }; + + try { + return await doCreateMessage(conversationId); + } catch (error) { + return this.handleStaleConversationError( + error, + instance, + conversationId, + messageBody, + messageBodyForRetry, + 'createMessage', + (newConvId) => doCreateMessage(newConvId), + ); } + } - return message; + private async handleStaleConversationError( + error: any, + instance: InstanceDto, + conversationId: number, + messageBody: any, + messageBodyForRetry: any, + functionName: string, + originalFunction: (newConversationId: number) => Promise, + ) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + this.logger.warn( + `Conversation ${conversationId} not found in Chatwoot. Retrying operation from ${functionName}...`, + ); + const bodyForRetry = messageBodyForRetry || messageBody; + + if (!bodyForRetry || !bodyForRetry.key?.remoteJid) { + this.logger.error(`Cannot retry ${functionName} without a message body for context.`); + return null; + } + + const { remoteJid } = bodyForRetry.key; + const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; + await this.cache.delete(cacheKey); + + const newConversationId = await this.createConversation(instance, bodyForRetry); + if (!newConversationId) { + this.logger.error(`Failed to create new conversation for ${remoteJid} during retry.`); + return null; + } + + this.logger.log(`Retrying ${functionName} for ${remoteJid} with new conversation ${newConversationId}`); + return await originalFunction(newConversationId); + } else { + this.logger.error(`Error in ${functionName}: ${error}`); + throw error; + } } public async getOpenConversationByContact( @@ -1006,6 +1102,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + messageBodyForRetry?: any, ) { if (sourceId && this.isImportHistoryAvailable()) { const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId); @@ -1016,54 +1113,65 @@ export class ChatwootService { } } } - const data = new FormData(); + const doSendData = async (convId: number) => { + const data = new FormData(); - if (content) { - data.append('content', content); - } - - data.append('message_type', messageType); - - data.append('attachments[]', fileData, { filename: fileName }); - - const sourceReplyId = quotedMsg?.chatwootMessageId || null; - - if (messageBody && instance) { - const replyToIds = await this.getReplyToIds(messageBody, instance); - - if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { - const content = JSON.stringify({ - ...replyToIds, - }); - data.append('content_attributes', content); + if (content) { + data.append('content', content); } - } - if (sourceReplyId) { - data.append('source_reply_id', sourceReplyId.toString()); - } + data.append('message_type', messageType); - if (sourceId) { - data.append('source_id', sourceId); - } + data.append('attachments[]', fileStream, { filename: fileName }); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, + const sourceReplyId = quotedMsg?.chatwootMessageId || null; + + if (messageBody && instance) { + const replyToIds = await this.getReplyToIds(messageBody, instance); + + if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { + const content = JSON.stringify({ + ...replyToIds, + }); + data.append('content_attributes', content); + } + } + + if (sourceReplyId) { + data.append('source_reply_id', sourceReplyId.toString()); + } + + if (sourceId) { + data.append('source_id', sourceId); + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${convId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + const { data: responseData } = await axios.request(config); + return responseData; }; try { - const { data } = await axios.request(config); - - return data; + return await doSendData(conversationId); } catch (error) { - this.logger.error(error); + return this.handleStaleConversationError( + error, + instance, + conversationId, + messageBody, + messageBodyForRetry, + 'sendData', + (newConvId) => doSendData(newConvId), + ); } } @@ -2304,6 +2412,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2323,6 +2432,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2348,6 +2458,7 @@ export class ChatwootService { }, 'WAID:' + body.key.id, quotedMsg, + body, ); if (!send) { this.logger.warn('message not sent'); @@ -2399,6 +2510,8 @@ export class ChatwootService { instance, body, 'WAID:' + body.key.id, + quotedMsg, + null, ); if (!send) { @@ -2440,6 +2553,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2459,6 +2573,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2587,6 +2702,7 @@ export class ChatwootService { }, 'WAID:' + body.key.id, null, + body, ); if (!send) { @@ -2673,15 +2789,30 @@ export class ChatwootService { await this.createBotMessage(instance, msgStatus, 'incoming'); } - if (event === 'connection.update') { - if (body.status === 'open') { - // if we have qrcode count then we understand that a new connection was established - if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { - const msgConnection = i18next.t('cw.inbox.connected'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0; - chatwootImport.clearAll(instance); - } + if (event === 'connection.update' && body.status === 'open') { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + if (!waInstance) return; + + const now = Date.now(); + const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0); + + // Se a conexão foi estabelecida via QR code, notifica imediatamente. + if (waInstance.qrCode && waInstance.qrCode.count > 0) { + const msgConnection = i18next.t('cw.inbox.connected'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + waInstance.qrCode.count = 0; + waInstance.lastConnectionNotification = now; + chatwootImport.clearAll(instance); + } + // Se não foi via QR code, verifica o throttling. + else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) { + const msgConnection = i18next.t('cw.inbox.connected'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + waInstance.lastConnectionNotification = now; + } else { + this.logger.warn( + `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, + ); } } @@ -2861,7 +2992,8 @@ export class ChatwootService { and created_at >= now() - interval '6h' order by created_at desc`; - const messagesData = (await this.pgClient.query(sqlMessages))?.rows; + const pgClient = await this.getPgClient(); + const messagesData = (await pgClient.query(sqlMessages))?.rows; const ids: string[] = messagesData .filter((message) => !!message.source_id) .map((message) => message.source_id.replace('WAID:', '')); diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index e02abd91..7be07431 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -23,15 +23,17 @@ const configService: ConfigService = new ConfigService(); const resources: any = {}; -languages.forEach((language) => { - const languagePath = path.join(translationsPath, `${language}.json`); - if (fs.existsSync(languagePath)) { - const translationContent = fs.readFileSync(languagePath, 'utf8'); - resources[language] = { - translation: JSON.parse(translationContent), - }; - } -}); +if (translationsPath) { + languages.forEach((language) => { + const languagePath = path.join(translationsPath, `${language}.json`); + if (fs.existsSync(languagePath)) { + const translationContent = fs.readFileSync(languagePath, 'utf8'); + resources[language] = { + translation: JSON.parse(translationContent), + }; + } + }); +} i18next.init({ resources,