From 6c150eed6db71bb2d461bcb254281fcf4c1f1557 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 23 Sep 2025 18:40:19 -0300 Subject: [PATCH 01/16] chore(docker): add Kafka and frontend services to Docker configurations - Introduced Kafka and Zookeeper services in a new docker-compose file for better message handling. - Added frontend service to both development and production docker-compose files for improved UI management. - Updated evolution-manager-v2 submodule to the latest commit. - Updated CHANGELOG for version 2.3.5 release. --- CHANGELOG.md | 4 + Docker/kafka/README.md | 139 +++++++++++++++++++++++++++++ Docker/kafka/docker-compose.yaml | 51 +++++++++++ Docker/swarm/evolution_api_v2.yaml | 2 +- docker-compose.dev.yaml | 10 +++ docker-compose.yaml | 9 ++ evolution-manager-v2 | 2 +- 7 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 Docker/kafka/README.md create mode 100644 Docker/kafka/docker-compose.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3b92f9..d1ada766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.3.5 (develop) + +### + # 2.3.4 (2025-09-23) ### Features diff --git a/Docker/kafka/README.md b/Docker/kafka/README.md new file mode 100644 index 00000000..6c17ab41 --- /dev/null +++ b/Docker/kafka/README.md @@ -0,0 +1,139 @@ +# Kafka Docker Setup for Evolution API + +This directory contains the Docker Compose configuration for running Apache Kafka locally for development and testing with Evolution API. + +## Services + +### Zookeeper +- **Container**: `zookeeper` +- **Image**: `confluentinc/cp-zookeeper:7.5.0` +- **Port**: `2181` +- **Purpose**: Coordination service for Kafka cluster + +### Kafka +- **Container**: `kafka` +- **Image**: `confluentinc/cp-kafka:7.5.0` +- **Ports**: + - `9092` - PLAINTEXT_HOST (localhost access) + - `29092` - PLAINTEXT (internal container access) + - `9094` - OUTSIDE (external Docker access) +- **Purpose**: Message broker for event streaming + +## Quick Start + +### 1. Start Kafka Services +```bash +cd Docker/kafka +docker-compose up -d +``` + +### 2. Verify Services +```bash +# Check if containers are running +docker-compose ps + +# Check Kafka logs +docker-compose logs kafka + +# Check Zookeeper logs +docker-compose logs zookeeper +``` + +### 3. Test Kafka Connection +```bash +# Create a test topic +docker exec kafka kafka-topics --create --topic test-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 + +# List topics +docker exec kafka kafka-topics --list --bootstrap-server localhost:9092 + +# Produce messages +docker exec -it kafka kafka-console-producer --topic test-topic --bootstrap-server localhost:9092 + +# Consume messages (in another terminal) +docker exec -it kafka kafka-console-consumer --topic test-topic --from-beginning --bootstrap-server localhost:9092 +``` + +## Evolution API Integration + +### Environment Variables +Configure these variables in your Evolution API `.env` file: + +```bash +# Kafka Configuration +KAFKA_ENABLED=true +KAFKA_CLIENT_ID=evolution-api +KAFKA_BROKERS=localhost:9092 +KAFKA_GLOBAL_ENABLED=true +KAFKA_CONSUMER_GROUP_ID=evolution-api-consumers +KAFKA_TOPIC_PREFIX=evolution +KAFKA_AUTO_CREATE_TOPICS=true + +# Event Configuration +KAFKA_EVENTS_APPLICATION_STARTUP=true +KAFKA_EVENTS_INSTANCE_CREATE=true +KAFKA_EVENTS_MESSAGES_UPSERT=true +# ... other events as needed +``` + +### Connection Endpoints +- **From Evolution API**: `localhost:9092` +- **From other Docker containers**: `kafka:29092` +- **From external applications**: `host.docker.internal:9094` + +## Data Persistence + +Data is persisted in Docker volumes: +- `zookeeper_data`: Zookeeper data and logs +- `kafka_data`: Kafka topic data and logs + +## Network + +Services run on the `evolution-net` network, allowing integration with other Evolution API services. + +## Stopping Services + +```bash +# Stop services +docker-compose down + +# Stop and remove volumes (WARNING: This will delete all data) +docker-compose down -v +``` + +## Troubleshooting + +### Connection Issues +1. Ensure ports 2181, 9092, 29092, and 9094 are not in use +2. Check if Docker network `evolution-net` exists +3. Verify firewall settings allow connections to these ports + +### Performance Tuning +The configuration includes production-ready settings: +- Log retention: 7 days (168 hours) +- Compression: gzip +- Auto-topic creation enabled +- Optimized segment and retention settings + +### Logs +```bash +# View all logs +docker-compose logs + +# Follow logs in real-time +docker-compose logs -f + +# View specific service logs +docker-compose logs kafka +docker-compose logs zookeeper +``` + +## Integration with Evolution API + +Once Kafka is running, Evolution API will automatically: +1. Connect to Kafka on startup (if `KAFKA_ENABLED=true`) +2. Create topics based on `KAFKA_TOPIC_PREFIX` +3. Start producing events to configured topics +4. Handle consumer groups for reliable message processing + +For more details on Kafka integration, see the main Evolution API documentation. 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 From 45858507418c14ddfbe9312317210f1e418a97f7 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 23 Sep 2025 18:42:07 -0300 Subject: [PATCH 02/16] chore(release): bump version to 2.3.5 and update bug report template - Updated package and lock files to version 2.3.5. - Modified bug report template to reflect the new version number. - Removed outdated Kafka Docker README file. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- Docker/kafka/README.md | 139 -------------------------- package-lock.json | 4 +- package.json | 2 +- 4 files changed, 4 insertions(+), 143 deletions(-) delete mode 100644 Docker/kafka/README.md 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/Docker/kafka/README.md b/Docker/kafka/README.md deleted file mode 100644 index 6c17ab41..00000000 --- a/Docker/kafka/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# Kafka Docker Setup for Evolution API - -This directory contains the Docker Compose configuration for running Apache Kafka locally for development and testing with Evolution API. - -## Services - -### Zookeeper -- **Container**: `zookeeper` -- **Image**: `confluentinc/cp-zookeeper:7.5.0` -- **Port**: `2181` -- **Purpose**: Coordination service for Kafka cluster - -### Kafka -- **Container**: `kafka` -- **Image**: `confluentinc/cp-kafka:7.5.0` -- **Ports**: - - `9092` - PLAINTEXT_HOST (localhost access) - - `29092` - PLAINTEXT (internal container access) - - `9094` - OUTSIDE (external Docker access) -- **Purpose**: Message broker for event streaming - -## Quick Start - -### 1. Start Kafka Services -```bash -cd Docker/kafka -docker-compose up -d -``` - -### 2. Verify Services -```bash -# Check if containers are running -docker-compose ps - -# Check Kafka logs -docker-compose logs kafka - -# Check Zookeeper logs -docker-compose logs zookeeper -``` - -### 3. Test Kafka Connection -```bash -# Create a test topic -docker exec kafka kafka-topics --create --topic test-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 - -# List topics -docker exec kafka kafka-topics --list --bootstrap-server localhost:9092 - -# Produce messages -docker exec -it kafka kafka-console-producer --topic test-topic --bootstrap-server localhost:9092 - -# Consume messages (in another terminal) -docker exec -it kafka kafka-console-consumer --topic test-topic --from-beginning --bootstrap-server localhost:9092 -``` - -## Evolution API Integration - -### Environment Variables -Configure these variables in your Evolution API `.env` file: - -```bash -# Kafka Configuration -KAFKA_ENABLED=true -KAFKA_CLIENT_ID=evolution-api -KAFKA_BROKERS=localhost:9092 -KAFKA_GLOBAL_ENABLED=true -KAFKA_CONSUMER_GROUP_ID=evolution-api-consumers -KAFKA_TOPIC_PREFIX=evolution -KAFKA_AUTO_CREATE_TOPICS=true - -# Event Configuration -KAFKA_EVENTS_APPLICATION_STARTUP=true -KAFKA_EVENTS_INSTANCE_CREATE=true -KAFKA_EVENTS_MESSAGES_UPSERT=true -# ... other events as needed -``` - -### Connection Endpoints -- **From Evolution API**: `localhost:9092` -- **From other Docker containers**: `kafka:29092` -- **From external applications**: `host.docker.internal:9094` - -## Data Persistence - -Data is persisted in Docker volumes: -- `zookeeper_data`: Zookeeper data and logs -- `kafka_data`: Kafka topic data and logs - -## Network - -Services run on the `evolution-net` network, allowing integration with other Evolution API services. - -## Stopping Services - -```bash -# Stop services -docker-compose down - -# Stop and remove volumes (WARNING: This will delete all data) -docker-compose down -v -``` - -## Troubleshooting - -### Connection Issues -1. Ensure ports 2181, 9092, 29092, and 9094 are not in use -2. Check if Docker network `evolution-net` exists -3. Verify firewall settings allow connections to these ports - -### Performance Tuning -The configuration includes production-ready settings: -- Log retention: 7 days (168 hours) -- Compression: gzip -- Auto-topic creation enabled -- Optimized segment and retention settings - -### Logs -```bash -# View all logs -docker-compose logs - -# Follow logs in real-time -docker-compose logs -f - -# View specific service logs -docker-compose logs kafka -docker-compose logs zookeeper -``` - -## Integration with Evolution API - -Once Kafka is running, Evolution API will automatically: -1. Connect to Kafka on startup (if `KAFKA_ENABLED=true`) -2. Create topics based on `KAFKA_TOPIC_PREFIX` -3. Start producing events to configured topics -4. Handle consumer groups for reliable message processing - -For more details on Kafka integration, see the main Evolution API documentation. diff --git a/package-lock.json b/package-lock.json index 5b558ca0..c5a78a76 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", diff --git a/package.json b/package.json index 748439d6..c48b2275 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", From d8268b0eb15bc517895d056f2220ef4693ad6eab Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 24 Sep 2025 13:59:23 -0300 Subject: [PATCH 03/16] fix(migration): resolve PostgreSQL migration error for Kafka integration - Corrected table reference in migration SQL to align with naming conventions. - Fixed foreign key constraint issue that caused migration failure. - Ensured successful setup of Kafka integration by addressing database migration errors. --- CHANGELOG.md | 8 ++++++++ .../20250918182355_add_kafka_integration/migration.sql | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ada766..a3b617ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 + ### # 2.3.4 (2025-09-23) 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; From 093515555d25fe2eaf0cb06732e70570ff3a7a76 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Thu, 25 Sep 2025 17:08:40 -0300 Subject: [PATCH 04/16] =?UTF-8?q?refactor(chatbot):=20refatorar=20conex?= =?UTF-8?q?=C3=A3o=20com=20PostgreSQL=20e=20melhorar=20tratamento=20de=20m?= =?UTF-8?q?ensagens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alterado método de obtenção da conexão PostgreSQL para ser assíncrono, melhorando a gestão de conexões. - Implementada lógica de retry para criação de mensagens e conversas, garantindo maior robustez em caso de falhas. - Ajustadas chamadas de consulta ao banco de dados para utilizar a nova abordagem de conexão. - Adicionada nova propriedade `messageBodyForRetry` para facilitar o reenvio de mensagens em caso de erro. --- .../chatwoot/services/chatwoot.service.ts | 210 ++++++++++++------ 1 file changed, 144 insertions(+), 66 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5020bfa6..bca90082 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -53,7 +53,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`; @@ -382,7 +384,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; @@ -392,18 +395,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; @@ -861,6 +864,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + messageBodyForRetry?: any, ) { const client = await this.clientCw(instance); @@ -869,32 +873,66 @@ 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) { + const errorMessage = error.toString().toLowerCase(); + const status = error.response?.status; + if (errorMessage.includes('not found') || status === 404) { + this.logger.warn(`Conversation ${conversationId} not found. Retrying...`); + const bodyForRetry = messageBodyForRetry || messageBody; + + if (!bodyForRetry) { + this.logger.error('Cannot retry createMessage without a message body for context.'); + return null; + } + + const remoteJid = bodyForRetry.key.remoteJid; + 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}`); + return null; + } + + this.logger.log(`Retrying message creation for ${remoteJid} with new conversation ${newConversationId}`); + return await doCreateMessage(newConversationId); + } else { + this.logger.error(`Error creating message: ${error}`); + throw error; + } } - - return message; } public async getOpenConversationByContact( @@ -987,6 +1025,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + messageBodyForRetry?: any, ) { if (sourceId && this.isImportHistoryAvailable()) { const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId); @@ -997,54 +1036,84 @@ 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[]', fileStream, { 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); + const errorMessage = error.toString().toLowerCase(); + const status = error.response?.status; + + if (errorMessage.includes('not found') || status === 404) { + this.logger.warn(`Conversation ${conversationId} not found. Retrying...`); + const bodyForRetry = messageBodyForRetry || messageBody; + + if (!bodyForRetry) { + this.logger.error('Cannot retry sendData without a message body for context.'); + return null; + } + + const remoteJid = bodyForRetry.key.remoteJid; + 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}`); + return null; + } + + this.logger.log(`Retrying sendData for ${remoteJid} with new conversation ${newConversationId}`); + return await doSendData(newConversationId); + } else { + this.logger.error(error); + return null; + } } } @@ -2032,6 +2101,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2051,6 +2121,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2076,6 +2147,7 @@ export class ChatwootService { }, 'WAID:' + body.key.id, quotedMsg, + body, ); if (!send) { this.logger.warn('message not sent'); @@ -2132,6 +2204,8 @@ export class ChatwootService { instance, body, 'WAID:' + body.key.id, + quotedMsg, + null, ); if (!send) { @@ -2173,6 +2247,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2192,6 +2267,7 @@ export class ChatwootService { body, 'WAID:' + body.key.id, quotedMsg, + null, ); if (!send) { @@ -2262,6 +2338,7 @@ export class ChatwootService { }, 'WAID:' + body.key.id, null, + body, ); if (!send) { this.logger.warn('edited message not sent'); @@ -2515,7 +2592,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:', '')); From 58b5561f72e20a729733b66b406b68899f4d7fd8 Mon Sep 17 00:00:00 2001 From: Vitor Manoel Santos Moura <72520858+Vitordotpy@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:30:30 -0300 Subject: [PATCH 05/16] Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aplicação de desestruturação de objetos que é uma boa prática do ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index bca90082..e44ea86e 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -916,7 +916,7 @@ export class ChatwootService { return null; } - const remoteJid = bodyForRetry.key.remoteJid; + const {remoteJid} = bodyForRetry.key; const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; await this.cache.delete(cacheKey); From 8697329f7197a41be3898b2bfe4d9c9be06792aa Mon Sep 17 00:00:00 2001 From: Vitor Manoel Santos Moura <72520858+Vitordotpy@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:30:43 -0300 Subject: [PATCH 06/16] Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aplicação de desestruturação de objetos que é uma boa prática do ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .../integrations/chatbot/chatwoot/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index e44ea86e..058ccd7b 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1098,7 +1098,7 @@ export class ChatwootService { return null; } - const remoteJid = bodyForRetry.key.remoteJid; + const {remoteJid} = bodyForRetry.key; const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; await this.cache.delete(cacheKey); From 5dc1d02d0aa885e3839424221dbfc9062a3d7646 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Thu, 25 Sep 2025 17:38:10 -0300 Subject: [PATCH 07/16] refactor(chatbot): melhorar tratamento de erros em mensagens no Chatwoot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementada a função `handleStaleConversationError` para centralizar a lógica de tratamento de erros relacionados a conversas não encontradas. - A lógica de retry foi aprimorada para as funções `createMessage` e `sendData`, garantindo que as operações sejam reprocessadas corretamente em caso de falhas. - Removido código duplicado e melhorada a legibilidade do serviço Chatwoot. --- .../chatwoot/services/chatwoot.service.ts | 105 +++++++++--------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 058ccd7b..fd31da84 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -905,33 +905,53 @@ export class ChatwootService { try { return await doCreateMessage(conversationId); } catch (error) { - const errorMessage = error.toString().toLowerCase(); - const status = error.response?.status; - if (errorMessage.includes('not found') || status === 404) { - this.logger.warn(`Conversation ${conversationId} not found. Retrying...`); - const bodyForRetry = messageBodyForRetry || messageBody; + return this.handleStaleConversationError( + error, + instance, + conversationId, + messageBody, + messageBodyForRetry, + 'createMessage', + (newConvId) => doCreateMessage(newConvId), + ); + } + } - if (!bodyForRetry) { - this.logger.error('Cannot retry createMessage without a message body for context.'); - return null; - } + 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; - 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}`); - return null; - } - - this.logger.log(`Retrying message creation for ${remoteJid} with new conversation ${newConversationId}`); - return await doCreateMessage(newConversationId); - } else { - this.logger.error(`Error creating message: ${error}`); - throw error; + 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; } } @@ -1086,34 +1106,15 @@ export class ChatwootService { try { return await doSendData(conversationId); } catch (error) { - const errorMessage = error.toString().toLowerCase(); - const status = error.response?.status; - - if (errorMessage.includes('not found') || status === 404) { - this.logger.warn(`Conversation ${conversationId} not found. Retrying...`); - const bodyForRetry = messageBodyForRetry || messageBody; - - if (!bodyForRetry) { - this.logger.error('Cannot retry sendData 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}`); - return null; - } - - this.logger.log(`Retrying sendData for ${remoteJid} with new conversation ${newConversationId}`); - return await doSendData(newConversationId); - } else { - this.logger.error(error); - return null; - } + return this.handleStaleConversationError( + error, + instance, + conversationId, + messageBody, + messageBodyForRetry, + 'sendData', + (newConvId) => doSendData(newConvId), + ); } } From 069786b9fecded1f5ea9dabdb0d79b7a96fdcc5b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 26 Sep 2025 12:56:34 -0300 Subject: [PATCH 08/16] chore(deps): update baileys package to version 7.0.0-rc.4 - Bumped baileys dependency version in package.json and package-lock.json to 7.0.0-rc.4 for improved functionality and bug fixes. --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5a78a76..466a4ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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.4", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -5993,9 +5993,9 @@ } }, "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.4", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.4.tgz", + "integrity": "sha512-mmXWTRYg3lcs20s/qHYZ5PQP6/qwID8Kgft7m2nMaHdAF13GbRSq59IcZolcbQSJ9UIrw26HPCFA2xYdBHUcqA==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c48b2275..b3b14121 100644 --- a/package.json +++ b/package.json @@ -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.4", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", From da6f1bd54062c1e6ece23c167eb810cbbacd954e Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 26 Sep 2025 12:58:36 -0300 Subject: [PATCH 09/16] chore(changelog): update CHANGELOG for Baileys v7.0.0-rc.4 and PostgreSQL connection improvements - Added entry for Baileys version update to v7.0.0-rc.4. - Refactored PostgreSQL connection handling and enhanced message processing capabilities. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b617ea..9dfb834c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - 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 + ### From 22465c0a56ffd7f3bf4b17e7caf4b3d9b88209bb Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 26 Sep 2025 13:00:52 -0300 Subject: [PATCH 10/16] fix: corrigido incompatibilidade no use voise call da wavoip com versao nova da baileys --- .../channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts | 2 +- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 34ef1366..02dfd560 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4561,8 +4561,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; } From c31b62fb3dfadf12233f336f15bb2bcc42579b25 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 26 Sep 2025 16:00:39 -0300 Subject: [PATCH 11/16] =?UTF-8?q?fix(baileys):=20corrigir=20verifica=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20mensagem=20no=20servi=C3=A7o=20Baileys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajustada a lógica de verificação para garantir que o ID da mensagem seja definido apenas quando disponível, evitando possíveis erros de referência. - Atualizada a definição do caminho de traduções para suportar a estrutura de diretórios em produção. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 5 ++++- src/utils/i18n.ts | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 02dfd560..ccb594f2 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1498,7 +1498,10 @@ export class BaileysStartupService extends ChannelStartupService { `) as any[]; findMessage = messages[0] || null; - if (findMessage) message.messageId = findMessage.id; + if (!findMessage?.id) { + continue; + } + message.messageId = findMessage.id; } if (update.message === null && update.status === undefined) { diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index af737ed0..09b9feb0 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -3,10 +3,11 @@ import fs from 'fs'; import i18next from 'i18next'; import path from 'path'; -const __dirname = path.resolve(process.cwd(), 'src', 'utils'); +const translationsPath = fs.existsSync(path.resolve(process.cwd(), 'dist')) + ? path.resolve(process.cwd(), 'dist', 'translations') + : path.resolve(process.cwd(), 'src', 'utils', 'translations'); const languages = ['en', 'pt-BR', 'es']; -const translationsPath = path.join(__dirname, 'translations'); const configService: ConfigService = new ConfigService(); const resources: any = {}; From eeb324227b55c9f9b1a37013438d57294d3e083b Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Fri, 26 Sep 2025 16:12:40 -0300 Subject: [PATCH 12/16] =?UTF-8?q?fix(baileys):=20adicionar=20log=20de=20av?= =?UTF-8?q?iso=20para=20mensagens=20n=C3=A3o=20encontradas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementada uma mensagem de aviso no serviço Baileys quando a mensagem original não é encontrada durante a atualização, melhorando a rastreabilidade de erros. - Ajustada a lógica de verificação do caminho de traduções para garantir que o diretório correto seja utilizado, com tratamento de erro caso não seja encontrado. --- .../whatsapp/whatsapp.baileys.service.ts | 1 + src/utils/i18n.ts | 37 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ccb594f2..82252dcb 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1499,6 +1499,7 @@ export class BaileysStartupService extends ChannelStartupService { findMessage = messages[0] || null; if (!findMessage?.id) { + this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); continue; } message.messageId = findMessage.id; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 09b9feb0..e8aea42b 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -3,24 +3,37 @@ import fs from 'fs'; import i18next from 'i18next'; import path from 'path'; -const translationsPath = fs.existsSync(path.resolve(process.cwd(), 'dist')) - ? path.resolve(process.cwd(), 'dist', 'translations') - : path.resolve(process.cwd(), 'src', 'utils', 'translations'); +const distPath = path.resolve(process.cwd(), 'dist', 'translations'); +const srcPath = path.resolve(process.cwd(), 'src', 'utils', 'translations'); + +let translationsPath; + +if (fs.existsSync(distPath)) { + translationsPath = distPath; +} else if (fs.existsSync(srcPath)) { + translationsPath = srcPath; +} else { + console.error('Translations directory not found in dist or src.'); + // Fallback to a non-existent path or handle error appropriately + translationsPath = ''; +} const languages = ['en', 'pt-BR', 'es']; 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, From 0d8e8bc0fb7ee331437cf313e4220cdf590b7176 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Sun, 28 Sep 2025 22:19:36 -0300 Subject: [PATCH 13/16] =?UTF-8?q?fix(chatwoot):=20corrige=20reabertura=20d?= =?UTF-8?q?e=20conversas=20e=20loop=20de=20conex=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit aborda duas questões críticas na integração com o Chatwoot para melhorar a estabilidade e a experiência do agente. Primeiro, as conversas que já estavam marcadas como "resolvidas" no Chatwoot não eram reabertas automaticamente quando o cliente enviava uma nova mensagem. Isso foi corrigido para que o sistema verifique o status da conversa e a reabra, garantindo que nenhuma nova interação seja perdida. Segundo, um bug no tratamento do evento de conexão fazia com que a mensagem de status "Conexão estabelecida com sucesso" fosse enviada repetidamente, poluindo o histórico da conversa. A lógica foi ajustada para garantir que esta notificação seja enviada apenas uma vez por evento de conexão. --- .../chatwoot/services/chatwoot.service.ts | 82 +++++++++++++------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index fd31da84..5025da31 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -747,34 +747,49 @@ 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 = contactConversations.payload.find( + (conversation) => + conversation && conversation.status !== 'resolved' && conversation.inbox_id == 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 = contactConversations.payload.find( + (conversation) => conversation.inbox_id == filterInbox.id, + ); + + if (inboxConversation) { + this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(inboxConversation)}`); + if (this.provider.conversationPending && inboxConversation.status !== 'open') { + await client.conversations.toggleStatus({ + accountId: this.provider.accountId, + conversationId: inboxConversation.id, + data: { + status: 'pending', + }, + }); + this.logger.verbose(`Reopened resolved conversation ID: ${inboxConversation.id}`); + } + } } + } else { + inboxConversation = contactConversations.payload.find( + (conversation) => + conversation && conversation.status !== 'resolved' && conversation.inbox_id == 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; } const data = { @@ -2407,12 +2422,25 @@ export class ChatwootService { if (event === 'connection.update') { if (body.status === 'open') { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; // if we have qrcode count then we understand that a new connection was established - if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { + if (waInstance && waInstance.qrCode.count > 0) { const msgConnection = i18next.t('cw.inbox.connected'); await this.createBotMessage(instance, msgConnection, 'incoming'); - this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0; + waInstance.qrCode.count = 0; + + waInstance.lastConnectionNotification = Date.now(); + chatwootImport.clearAll(instance); + } else if (waInstance) { + const timeSinceLastNotification = Date.now() - (waInstance.lastConnectionNotification || 0); + const minIntervalMs = 30000; // 30 seconds + + if (timeSinceLastNotification < minIntervalMs) { + this.logger.warn( + `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, + ); + } } } } From f7862637b164c1e625b8bdfa4bd1ac27cc6c4c6e Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Sun, 28 Sep 2025 22:38:45 -0300 Subject: [PATCH 14/16] =?UTF-8?q?fix(chatwoot):=20otimizar=20l=C3=B3gica?= =?UTF-8?q?=20de=20reabertura=20de=20conversas=20e=20notifica=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20conex=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit introduz melhorias na integração com o Chatwoot, focando na reabertura de conversas e na notificação de conexão. A lógica foi refatorada para centralizar a busca por conversas abertas e a reabertura de conversas resolvidas, garantindo que interações não sejam perdidas. Além disso, foi implementado um intervalo mínimo para notificações de conexão, evitando mensagens excessivas e melhorando a experiência do usuário. --- .../chatwoot/services/chatwoot.service.ts | 124 ++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5025da31..5e53b2ee 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; @@ -747,43 +749,14 @@ export class ChatwootService { return null; } - let inboxConversation = null; + let inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); - if (this.provider.reopenConversation) { - inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + if (!inboxConversation && this.provider.reopenConversation) { + inboxConversation = await this.findAndReopenResolvedConversation( + client, + contactConversations.payload, + filterInbox.id, ); - - if (inboxConversation) { - this.logger.verbose( - `Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`, - ); - } else { - inboxConversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); - - if (inboxConversation) { - this.logger.verbose(`Found resolved conversation to reopen: ${JSON.stringify(inboxConversation)}`); - if (this.provider.conversationPending && inboxConversation.status !== 'open') { - await client.conversations.toggleStatus({ - accountId: this.provider.accountId, - conversationId: inboxConversation.id, - data: { - status: 'pending', - }, - }); - this.logger.verbose(`Reopened resolved conversation ID: ${inboxConversation.id}`); - } - } - } - } else { - inboxConversation = contactConversations.payload.find( - (conversation) => - conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`); } if (inboxConversation) { @@ -832,6 +805,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)) { @@ -2420,28 +2432,30 @@ export class ChatwootService { await this.createBotMessage(instance, msgStatus, 'incoming'); } - if (event === 'connection.update') { - if (body.status === 'open') { - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - // if we have qrcode count then we understand that a new connection was established - if (waInstance && waInstance.qrCode.count > 0) { - const msgConnection = i18next.t('cw.inbox.connected'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - waInstance.qrCode.count = 0; + if (event === 'connection.update' && body.status === 'open') { + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + if (!waInstance) return; - waInstance.lastConnectionNotification = Date.now(); + const now = Date.now(); + const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0); - chatwootImport.clearAll(instance); - } else if (waInstance) { - const timeSinceLastNotification = Date.now() - (waInstance.lastConnectionNotification || 0); - const minIntervalMs = 30000; // 30 seconds - - if (timeSinceLastNotification < minIntervalMs) { - this.logger.warn( - `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, - ); - } - } + // 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)`, + ); } } From c132379b3ab0cbd501f0b5881c61b0291d967a54 Mon Sep 17 00:00:00 2001 From: Vitordotpy Date: Mon, 29 Sep 2025 15:26:24 -0300 Subject: [PATCH 15/16] =?UTF-8?q?fix(chatwoot):=20ajustar=20l=C3=B3gica=20?= =?UTF-8?q?de=20verifica=C3=A7=C3=A3o=20de=20conversas=20e=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este commit modifica a lógica de verificação de conversas no serviço Chatwoot, garantindo que a busca por conversas ativas seja priorizada em relação ao uso de cache. A verificação de cache foi removida em pontos críticos para evitar que conversas desatualizadas sejam utilizadas, melhorando a precisão na recuperação de dados. Além disso, a lógica de reabertura de conversas foi refinada para garantir que as interações sejam tratadas corretamente, mantendo a experiência do usuário mais fluida. --- .../chatwoot/services/chatwoot.service.ts | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 5e53b2ee..f439faed 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -606,12 +606,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)) { @@ -623,11 +618,7 @@ export class ChatwootService { break; } await new Promise((res) => setTimeout(res, 300)); - if (await this.cache.has(cacheKey)) { - const conversationId = (await this.cache.get(cacheKey)) as number; - this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`); - return conversationId; - } + // Removed cache check here to ensure we always check Chatwoot } } @@ -637,12 +628,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; @@ -749,14 +737,25 @@ export class ChatwootService { return null; } - let inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + let inboxConversation = null; - if (!inboxConversation && this.provider.reopenConversation) { - inboxConversation = await this.findAndReopenResolvedConversation( - client, - contactConversations.payload, - filterInbox.id, - ); + if (this.provider.reopenConversation) { + inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id); + + if (inboxConversation) { + 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) { @@ -765,6 +764,14 @@ export class ChatwootService { 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 = { contact_id: contactId.toString(), inbox_id: filterInbox.id.toString(), From 53cd7d5d1319f29c60485b6a9d35f76c9ae90378 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 6 Oct 2025 14:28:24 -0300 Subject: [PATCH 16/16] chore(deps): update baileys package to version 7.0.0-rc.5 - Bumped baileys dependency version in package.json and package-lock.json to 7.0.0-rc.5 for improved functionality and bug fixes. - Added p-queue and p-timeout packages for enhanced performance and timeout management. --- package-lock.json | 38 ++++++++++++++++--- package.json | 2 +- .../whatsapp/whatsapp.baileys.service.ts | 14 +++---- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 466a4ef9..815b9775 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc.4", + "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.4", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.4.tgz", - "integrity": "sha512-mmXWTRYg3lcs20s/qHYZ5PQP6/qwID8Kgft7m2nMaHdAF13GbRSq59IcZolcbQSJ9UIrw26HPCFA2xYdBHUcqA==", + "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 b3b14121..bff453fb 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc.4", + "baileys": "^7.0.0-rc.5", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 82252dcb..d60b6bab 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3371,18 +3371,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, ); }), );