mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-11 02:49:36 -06:00
Merge branch 'develop' into main
This commit is contained in:
commit
a5a46dc72a
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -59,7 +59,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
- OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0]
|
- OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0]
|
||||||
- Node.js version: [e.g. 18.17.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]
|
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
|
||||||
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
15
CHANGELOG.md
15
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)
|
# 2.3.4 (2025-09-23)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
51
Docker/kafka/docker-compose.yaml
Normal file
51
Docker/kafka/docker-compose.yaml
Normal file
@ -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
|
||||||
@ -2,7 +2,7 @@ version: "3.7"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
evolution_v2:
|
evolution_v2:
|
||||||
image: evoapicloud/evolution-api:v2.3.1
|
image: evoapicloud/evolution-api:v2.3.5
|
||||||
volumes:
|
volumes:
|
||||||
- evolution_instances:/evolution/instances
|
- evolution_instances:/evolution/instances
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -15,6 +15,16 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- 8080
|
- 8080
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: evolution_frontend
|
||||||
|
image: evolution/manager:local
|
||||||
|
build: ./evolution-manager-v2
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
networks:
|
||||||
|
- evolution-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
evolution_instances:
|
evolution_instances:
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,15 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "8080"
|
- "8080"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: evolution_frontend
|
||||||
|
image: evoapicloud/evolution-manager:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
networks:
|
||||||
|
- evolution-net
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: evolution_redis
|
container_name: evolution_redis
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit fcb38dd407b89697b7a7154cfd873f76729e6ece
|
Subproject commit e510b5f17fc75cbddeaaba102ddd568e4b127455
|
||||||
42
package-lock.json
generated
42
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.3.4",
|
"version": "2.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.3.4",
|
"version": "2.3.5",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adiwajshing/keyed-db": "^0.2.4",
|
"@adiwajshing/keyed-db": "^0.2.4",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"amqplib": "^0.10.5",
|
"amqplib": "^0.10.5",
|
||||||
"audio-decode": "^2.2.3",
|
"audio-decode": "^2.2.3",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"baileys": "^7.0.0-rc.3",
|
"baileys": "^7.0.0-rc.5",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.7.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@ -5993,19 +5993,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baileys": {
|
"node_modules/baileys": {
|
||||||
"version": "7.0.0-rc.3",
|
"version": "7.0.0-rc.5",
|
||||||
"resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.3.tgz",
|
"resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.5.tgz",
|
||||||
"integrity": "sha512-SVRLTDMKnWhX2P+bANVO1s/IigmGsN2UMbR69ftwsj+DtHxmSn8qjWftVZ//dTOhkncU7xZhTEOWtsOrPoemvQ==",
|
"integrity": "sha512-y95gW7UtKbD4dQb46G75rnr0U0LtnBItA002ARggDiCgm92Z8wnM+wxqC8OI/sDFanz3TgzqE4t7MPwNusUqUQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/node-cache": "^1.4.0",
|
"@cacheable/node-cache": "^1.4.0",
|
||||||
"@hapi/boom": "^9.1.3",
|
"@hapi/boom": "^9.1.3",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"axios": "^1.6.0",
|
|
||||||
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git",
|
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
"music-metadata": "^11.7.0",
|
"music-metadata": "^11.7.0",
|
||||||
|
"p-queue": "^9.0.0",
|
||||||
"pino": "^9.6",
|
"pino": "^9.6",
|
||||||
"protobufjs": "^7.2.4",
|
"protobufjs": "^7.2.4",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
@ -12169,6 +12169,34 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/p-try": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.3.4",
|
"version": "2.3.5",
|
||||||
"description": "Rest api for communication with WhatsApp",
|
"description": "Rest api for communication with WhatsApp",
|
||||||
"main": "./dist/main.js",
|
"main": "./dist/main.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
@ -77,7 +77,7 @@
|
|||||||
"amqplib": "^0.10.5",
|
"amqplib": "^0.10.5",
|
||||||
"audio-decode": "^2.2.3",
|
"audio-decode": "^2.2.3",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"baileys": "^7.0.0-rc.3",
|
"baileys": "^7.0.0-rc.5",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.7.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "public"."Kafka" (
|
CREATE TABLE "Kafka" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
"events" JSONB NOT NULL,
|
"events" JSONB NOT NULL,
|
||||||
@ -11,7 +11,7 @@ CREATE TABLE "public"."Kafka" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "public"."Kafka"("instanceId");
|
CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "Kafka"("instanceId");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- 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;
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export const useVoiceCallsBaileys = async (
|
|||||||
|
|
||||||
socket.on('assertSessions', async (jids, force, callback) => {
|
socket.on('assertSessions', async (jids, force, callback) => {
|
||||||
try {
|
try {
|
||||||
const response = await baileys_sock.assertSessions(jids, force);
|
const response = await baileys_sock.assertSessions(jids);
|
||||||
|
|
||||||
callback(response);
|
callback(response);
|
||||||
|
|
||||||
|
|||||||
@ -1512,7 +1512,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
`) as any[];
|
`) as any[];
|
||||||
findMessage = messages[0] || null;
|
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) {
|
if (update.message === null && update.status === undefined) {
|
||||||
@ -3395,18 +3399,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numberJid = numberVerified?.jid || user.jid;
|
const numberJid = numberVerified?.jid || user.jid;
|
||||||
const lid =
|
// const lid =
|
||||||
typeof numberVerified?.lid === 'string'
|
// typeof numberVerified?.lid === 'string'
|
||||||
? numberVerified.lid
|
// ? numberVerified.lid
|
||||||
: numberJid.includes('@lid')
|
// : numberJid.includes('@lid')
|
||||||
? numberJid.split('@')[1]
|
// ? numberJid.split('@')[1]
|
||||||
: undefined;
|
// : undefined;
|
||||||
return new OnWhatsAppDto(
|
return new OnWhatsAppDto(
|
||||||
numberJid,
|
numberJid,
|
||||||
!!numberVerified?.exists,
|
!!numberVerified?.exists,
|
||||||
user.number,
|
user.number,
|
||||||
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
||||||
lid,
|
// lid,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -4589,8 +4593,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async baileysAssertSessions(jids: string[], force: boolean) {
|
public async baileysAssertSessions(jids: string[]) {
|
||||||
const response = await this.client.assertSessions(jids, force);
|
const response = await this.client.assertSessions(jids);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,8 @@ import mimeTypes from 'mime-types';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds
|
||||||
|
|
||||||
interface ChatwootMessage {
|
interface ChatwootMessage {
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
inboxId?: number;
|
inboxId?: number;
|
||||||
@ -72,7 +74,9 @@ export class ChatwootService {
|
|||||||
private readonly cache: CacheService,
|
private readonly cache: CacheService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private pgClient = postgresClient.getChatwootConnection();
|
private async getPgClient() {
|
||||||
|
return postgresClient.getChatwootConnection();
|
||||||
|
}
|
||||||
|
|
||||||
private async getProvider(instance: InstanceDto): Promise<ChatwootModel | null> {
|
private async getProvider(instance: InstanceDto): Promise<ChatwootModel | null> {
|
||||||
const cacheKey = `${instance.instanceName}:getProvider`;
|
const cacheKey = `${instance.instanceName}:getProvider`;
|
||||||
@ -401,7 +405,8 @@ export class ChatwootService {
|
|||||||
if (!uri) return false;
|
if (!uri) return false;
|
||||||
|
|
||||||
const sqlTags = `SELECT id, taggings_count FROM tags WHERE name = $1 LIMIT 1`;
|
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;
|
let tagId = tagData?.id;
|
||||||
const taggingsCount = tagData?.taggings_count || 0;
|
const taggingsCount = tagData?.taggings_count || 0;
|
||||||
|
|
||||||
@ -411,18 +416,18 @@ export class ChatwootService {
|
|||||||
DO UPDATE SET taggings_count = tags.taggings_count + 1
|
DO UPDATE SET taggings_count = tags.taggings_count + 1
|
||||||
RETURNING id`;
|
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
|
const sqlCheckTagging = `SELECT 1 FROM taggings
|
||||||
WHERE tag_id = $1 AND taggable_type = 'Contact' AND taggable_id = $2 AND context = 'labels' LIMIT 1`;
|
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) {
|
if (!taggingExists) {
|
||||||
const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at)
|
const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at)
|
||||||
VALUES ($1, 'Contact', $2, 'labels', NOW())`;
|
VALUES ($1, 'Contact', $2, 'labels', NOW())`;
|
||||||
|
|
||||||
await this.pgClient.query(sqlInsertLabel, [tagId, contactId]);
|
await pgClient.query(sqlInsertLabel, [tagId, contactId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -620,12 +625,7 @@ export class ChatwootService {
|
|||||||
this.logger.verbose(`--- Start createConversation ---`);
|
this.logger.verbose(`--- Start createConversation ---`);
|
||||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||||||
|
|
||||||
// If it already exists in the cache, return conversationId
|
// Always check Chatwoot first, cache only as fallback
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If lock already exists, wait until release or timeout
|
// If lock already exists, wait until release or timeout
|
||||||
if (await this.cache.has(lockKey)) {
|
if (await this.cache.has(lockKey)) {
|
||||||
@ -651,12 +651,9 @@ export class ChatwootService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
/*
|
/*
|
||||||
Double check after lock
|
Double check after lock - REMOVED
|
||||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
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);
|
const client = await this.clientCw(instance);
|
||||||
if (!client) return null;
|
if (!client) return null;
|
||||||
@ -763,26 +760,24 @@ export class ChatwootService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inboxConversation = contactConversations.payload.find(
|
let inboxConversation = null;
|
||||||
(conversation) => conversation.inbox_id == filterInbox.id,
|
|
||||||
);
|
|
||||||
if (inboxConversation) {
|
|
||||||
if (this.provider.reopenConversation) {
|
if (this.provider.reopenConversation) {
|
||||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id);
|
||||||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
|
||||||
await client.conversations.toggleStatus({
|
if (inboxConversation) {
|
||||||
accountId: this.provider.accountId,
|
this.logger.verbose(
|
||||||
conversationId: inboxConversation.id,
|
`Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`,
|
||||||
data: {
|
);
|
||||||
status: 'pending',
|
} else {
|
||||||
},
|
inboxConversation = await this.findAndReopenResolvedConversation(
|
||||||
});
|
client,
|
||||||
|
contactConversations.payload,
|
||||||
|
filterInbox.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
inboxConversation = contactConversations.payload.find(
|
inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id);
|
||||||
(conversation) =>
|
|
||||||
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
|
||||||
);
|
|
||||||
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,6 +786,13 @@ export class ChatwootService {
|
|||||||
this.cache.set(cacheKey, inboxConversation.id);
|
this.cache.set(cacheKey, inboxConversation.id);
|
||||||
return 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 = {
|
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<any | null> {
|
||||||
|
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<inbox | null> {
|
public async getInbox(instance: InstanceDto): Promise<inbox | null> {
|
||||||
const cacheKey = `${instance.instanceName}:getInbox`;
|
const cacheKey = `${instance.instanceName}:getInbox`;
|
||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
@ -880,6 +921,7 @@ export class ChatwootService {
|
|||||||
messageBody?: any,
|
messageBody?: any,
|
||||||
sourceId?: string,
|
sourceId?: string,
|
||||||
quotedMsg?: MessageModel,
|
quotedMsg?: MessageModel,
|
||||||
|
messageBodyForRetry?: any,
|
||||||
) {
|
) {
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@ -888,13 +930,14 @@ export class ChatwootService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doCreateMessage = async (convId: number) => {
|
||||||
const replyToIds = await this.getReplyToIds(messageBody, instance);
|
const replyToIds = await this.getReplyToIds(messageBody, instance);
|
||||||
|
|
||||||
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
||||||
|
|
||||||
const message = await client.messages.create({
|
const message = await client.messages.create({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
conversationId: conversationId,
|
conversationId: convId,
|
||||||
data: {
|
data: {
|
||||||
content: content,
|
content: content,
|
||||||
message_type: messageType,
|
message_type: messageType,
|
||||||
@ -914,6 +957,59 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await doCreateMessage(conversationId);
|
||||||
|
} catch (error) {
|
||||||
|
return this.handleStaleConversationError(
|
||||||
|
error,
|
||||||
|
instance,
|
||||||
|
conversationId,
|
||||||
|
messageBody,
|
||||||
|
messageBodyForRetry,
|
||||||
|
'createMessage',
|
||||||
|
(newConvId) => doCreateMessage(newConvId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleStaleConversationError(
|
||||||
|
error: any,
|
||||||
|
instance: InstanceDto,
|
||||||
|
conversationId: number,
|
||||||
|
messageBody: any,
|
||||||
|
messageBodyForRetry: any,
|
||||||
|
functionName: string,
|
||||||
|
originalFunction: (newConversationId: number) => Promise<any>,
|
||||||
|
) {
|
||||||
|
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(
|
public async getOpenConversationByContact(
|
||||||
@ -1006,6 +1102,7 @@ export class ChatwootService {
|
|||||||
messageBody?: any,
|
messageBody?: any,
|
||||||
sourceId?: string,
|
sourceId?: string,
|
||||||
quotedMsg?: MessageModel,
|
quotedMsg?: MessageModel,
|
||||||
|
messageBodyForRetry?: any,
|
||||||
) {
|
) {
|
||||||
if (sourceId && this.isImportHistoryAvailable()) {
|
if (sourceId && this.isImportHistoryAvailable()) {
|
||||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
||||||
@ -1016,6 +1113,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const doSendData = async (convId: number) => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
@ -1024,7 +1122,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
data.append('message_type', messageType);
|
data.append('message_type', messageType);
|
||||||
|
|
||||||
data.append('attachments[]', fileData, { filename: fileName });
|
data.append('attachments[]', fileStream, { filename: fileName });
|
||||||
|
|
||||||
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
||||||
|
|
||||||
@ -1050,7 +1148,7 @@ export class ChatwootService {
|
|||||||
const config = {
|
const config = {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
maxBodyLength: Infinity,
|
maxBodyLength: Infinity,
|
||||||
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`,
|
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${convId}/messages`,
|
||||||
headers: {
|
headers: {
|
||||||
api_access_token: this.provider.token,
|
api_access_token: this.provider.token,
|
||||||
...data.getHeaders(),
|
...data.getHeaders(),
|
||||||
@ -1058,12 +1156,22 @@ export class ChatwootService {
|
|||||||
data: data,
|
data: data,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const { data: responseData } = await axios.request(config);
|
||||||
const { data } = await axios.request(config);
|
return responseData;
|
||||||
|
};
|
||||||
|
|
||||||
return data;
|
try {
|
||||||
|
return await doSendData(conversationId);
|
||||||
} catch (error) {
|
} 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,
|
body,
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
quotedMsg,
|
quotedMsg,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2323,6 +2432,7 @@ export class ChatwootService {
|
|||||||
body,
|
body,
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
quotedMsg,
|
quotedMsg,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2348,6 +2458,7 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
quotedMsg,
|
quotedMsg,
|
||||||
|
body,
|
||||||
);
|
);
|
||||||
if (!send) {
|
if (!send) {
|
||||||
this.logger.warn('message not sent');
|
this.logger.warn('message not sent');
|
||||||
@ -2399,6 +2510,8 @@ export class ChatwootService {
|
|||||||
instance,
|
instance,
|
||||||
body,
|
body,
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
|
quotedMsg,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2440,6 +2553,7 @@ export class ChatwootService {
|
|||||||
body,
|
body,
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
quotedMsg,
|
quotedMsg,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2459,6 +2573,7 @@ export class ChatwootService {
|
|||||||
body,
|
body,
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
quotedMsg,
|
quotedMsg,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2587,6 +2702,7 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
'WAID:' + body.key.id,
|
'WAID:' + body.key.id,
|
||||||
null,
|
null,
|
||||||
|
body,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
@ -2673,15 +2789,30 @@ export class ChatwootService {
|
|||||||
await this.createBotMessage(instance, msgStatus, 'incoming');
|
await this.createBotMessage(instance, msgStatus, 'incoming');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'connection.update') {
|
if (event === 'connection.update' && body.status === 'open') {
|
||||||
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) return;
|
||||||
if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) {
|
|
||||||
|
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');
|
const msgConnection = i18next.t('cw.inbox.connected');
|
||||||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||||||
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
|
waInstance.qrCode.count = 0;
|
||||||
|
waInstance.lastConnectionNotification = now;
|
||||||
chatwootImport.clearAll(instance);
|
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'
|
and created_at >= now() - interval '6h'
|
||||||
order by created_at desc`;
|
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
|
const ids: string[] = messagesData
|
||||||
.filter((message) => !!message.source_id)
|
.filter((message) => !!message.source_id)
|
||||||
.map((message) => message.source_id.replace('WAID:', ''));
|
.map((message) => message.source_id.replace('WAID:', ''));
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const configService: ConfigService = new ConfigService();
|
|||||||
|
|
||||||
const resources: any = {};
|
const resources: any = {};
|
||||||
|
|
||||||
|
if (translationsPath) {
|
||||||
languages.forEach((language) => {
|
languages.forEach((language) => {
|
||||||
const languagePath = path.join(translationsPath, `${language}.json`);
|
const languagePath = path.join(translationsPath, `${language}.json`);
|
||||||
if (fs.existsSync(languagePath)) {
|
if (fs.existsSync(languagePath)) {
|
||||||
@ -32,6 +33,7 @@ languages.forEach((language) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
i18next.init({
|
i18next.init({
|
||||||
resources,
|
resources,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user