Merge branch 'develop' into main

This commit is contained in:
dersonbsb2022 2025-10-06 15:21:10 -03:00 committed by GitHub
commit a5a46dc72a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 396 additions and 145 deletions

View File

@ -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:

View File

@ -1,3 +1,18 @@
# 2.3.5 (develop)
### Fixed
* **Kafka Migration**: Fixed PostgreSQL migration error for Kafka integration
- Corrected table reference from `"public"."Instance"` to `"Instance"` in foreign key constraint
- Fixed `ERROR: relation "public.Instance" does not exist` issue in migration `20250918182355_add_kafka_integration`
- Aligned table naming convention with other Evolution API migrations for consistency
- Resolved database migration failure that prevented Kafka integration setup
* **Update Baileys Version**: v7.0.0-rc.4
* Refactor connection with PostgreSQL and improve message handling
###
# 2.3.4 (2025-09-23)
### Features

View 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

View File

@ -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:

View File

@ -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:

View File

@ -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

@ -1 +1 @@
Subproject commit fcb38dd407b89697b7a7154cfd873f76729e6ece
Subproject commit e510b5f17fc75cbddeaaba102ddd568e4b127455

42
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "evolution-api",
"version": "2.3.4",
"version": "2.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "evolution-api",
"version": "2.3.4",
"version": "2.3.5",
"license": "Apache-2.0",
"dependencies": {
"@adiwajshing/keyed-db": "^0.2.4",
@ -21,7 +21,7 @@
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "^7.0.0-rc.3",
"baileys": "^7.0.0-rc.5",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
@ -5993,19 +5993,19 @@
}
},
"node_modules/baileys": {
"version": "7.0.0-rc.3",
"resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.3.tgz",
"integrity": "sha512-SVRLTDMKnWhX2P+bANVO1s/IigmGsN2UMbR69ftwsj+DtHxmSn8qjWftVZ//dTOhkncU7xZhTEOWtsOrPoemvQ==",
"version": "7.0.0-rc.5",
"resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.5.tgz",
"integrity": "sha512-y95gW7UtKbD4dQb46G75rnr0U0LtnBItA002ARggDiCgm92Z8wnM+wxqC8OI/sDFanz3TgzqE4t7MPwNusUqUQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@cacheable/node-cache": "^1.4.0",
"@hapi/boom": "^9.1.3",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git",
"lru-cache": "^11.1.0",
"music-metadata": "^11.7.0",
"p-queue": "^9.0.0",
"pino": "^9.6",
"protobufjs": "^7.2.4",
"ws": "^8.13.0"
@ -12169,6 +12169,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-queue": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.0.0.tgz",
"integrity": "sha512-KO1RyxstL9g1mK76530TExamZC/S2Glm080Nx8PE5sTd7nlduDQsAfEl4uXX+qZjLiwvDauvzXavufy3+rJ9zQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1",
"p-timeout": "^7.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.0.tgz",
"integrity": "sha512-DhZ7ydOE3JXtXzDf2wz/KEamkKAD7Il5So09I2tOz4i+9pLcdghDKKmODkkoHKJ0TyczmhcHNxyTeTrsENT81A==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "2.3.4",
"version": "2.3.5",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js",
"type": "commonjs",
@ -77,7 +77,7 @@
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "^7.0.0-rc.3",
"baileys": "^7.0.0-rc.5",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",

View File

@ -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;

View File

@ -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);

View File

@ -1512,7 +1512,11 @@ export class BaileysStartupService extends ChannelStartupService {
`) as any[];
findMessage = messages[0] || null;
if (findMessage) message.messageId = findMessage.id;
if (!findMessage?.id) {
this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`);
continue;
}
message.messageId = findMessage.id;
}
if (update.message === null && update.status === undefined) {
@ -3395,18 +3399,18 @@ export class BaileysStartupService extends ChannelStartupService {
}
const numberJid = numberVerified?.jid || user.jid;
const lid =
typeof numberVerified?.lid === 'string'
? numberVerified.lid
: numberJid.includes('@lid')
? numberJid.split('@')[1]
: undefined;
// const lid =
// typeof numberVerified?.lid === 'string'
// ? numberVerified.lid
// : numberJid.includes('@lid')
// ? numberJid.split('@')[1]
// : undefined;
return new OnWhatsAppDto(
numberJid,
!!numberVerified?.exists,
user.number,
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
lid,
// lid,
);
}),
);
@ -4589,8 +4593,8 @@ export class BaileysStartupService extends ChannelStartupService {
return response;
}
public async baileysAssertSessions(jids: string[], force: boolean) {
const response = await this.client.assertSessions(jids, force);
public async baileysAssertSessions(jids: string[]) {
const response = await this.client.assertSessions(jids);
return response;
}

View File

@ -33,6 +33,8 @@ import mimeTypes from 'mime-types';
import path from 'path';
import { Readable } from 'stream';
const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds
interface ChatwootMessage {
messageId?: number;
inboxId?: number;
@ -72,7 +74,9 @@ export class ChatwootService {
private readonly cache: CacheService,
) {}
private pgClient = postgresClient.getChatwootConnection();
private async getPgClient() {
return postgresClient.getChatwootConnection();
}
private async getProvider(instance: InstanceDto): Promise<ChatwootModel | null> {
const cacheKey = `${instance.instanceName}:getProvider`;
@ -401,7 +405,8 @@ export class ChatwootService {
if (!uri) return false;
const sqlTags = `SELECT id, taggings_count FROM tags WHERE name = $1 LIMIT 1`;
const tagData = (await this.pgClient.query(sqlTags, [nameInbox]))?.rows[0];
const pgClient = await this.getPgClient();
const tagData = (await pgClient.query(sqlTags, [nameInbox]))?.rows[0];
let tagId = tagData?.id;
const taggingsCount = tagData?.taggings_count || 0;
@ -411,18 +416,18 @@ export class ChatwootService {
DO UPDATE SET taggings_count = tags.taggings_count + 1
RETURNING id`;
tagId = (await this.pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id;
tagId = (await pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id;
const sqlCheckTagging = `SELECT 1 FROM taggings
WHERE tag_id = $1 AND taggable_type = 'Contact' AND taggable_id = $2 AND context = 'labels' LIMIT 1`;
const taggingExists = (await this.pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0;
const taggingExists = (await pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0;
if (!taggingExists) {
const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at)
VALUES ($1, 'Contact', $2, 'labels', NOW())`;
await this.pgClient.query(sqlInsertLabel, [tagId, contactId]);
await pgClient.query(sqlInsertLabel, [tagId, contactId]);
}
return true;
@ -620,12 +625,7 @@ export class ChatwootService {
this.logger.verbose(`--- Start createConversation ---`);
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
// If it already exists in the cache, return conversationId
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
return conversationId;
}
// Always check Chatwoot first, cache only as fallback
// If lock already exists, wait until release or timeout
if (await this.cache.has(lockKey)) {
@ -651,12 +651,9 @@ export class ChatwootService {
try {
/*
Double check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
Double check after lock - REMOVED
This was causing the system to use cached conversations instead of checking Chatwoot
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const client = await this.clientCw(instance);
if (!client) return null;
@ -763,34 +760,39 @@ export class ChatwootService {
return null;
}
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
);
if (inboxConversation) {
if (this.provider.reopenConversation) {
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
}
} else {
inboxConversation = contactConversations.payload.find(
(conversation) =>
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
let inboxConversation = null;
if (this.provider.reopenConversation) {
inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id);
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
this.logger.verbose(
`Found open conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`,
);
} else {
inboxConversation = await this.findAndReopenResolvedConversation(
client,
contactConversations.payload,
filterInbox.id,
);
}
} else {
inboxConversation = this.findOpenConversation(contactConversations.payload, filterInbox.id);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.warn(
`No active conversations found in Chatwoot, using cached conversation ID: ${conversationId} as fallback`,
);
return conversationId;
}
const data = {
@ -833,6 +835,45 @@ export class ChatwootService {
}
}
private findOpenConversation(conversations: any[], inboxId: number): any | null {
const openConversation = conversations.find(
(conversation) => conversation && conversation.status !== 'resolved' && conversation.inbox_id == inboxId,
);
if (openConversation) {
this.logger.verbose(`Found open conversation: ${JSON.stringify(openConversation)}`);
}
return openConversation || null;
}
private async findAndReopenResolvedConversation(
client: any,
conversations: any[],
inboxId: number,
): Promise<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> {
const cacheKey = `${instance.instanceName}:getInbox`;
if (await this.cache.has(cacheKey)) {
@ -880,6 +921,7 @@ export class ChatwootService {
messageBody?: any,
sourceId?: string,
quotedMsg?: MessageModel,
messageBodyForRetry?: any,
) {
const client = await this.clientCw(instance);
@ -888,32 +930,86 @@ export class ChatwootService {
return null;
}
const replyToIds = await this.getReplyToIds(messageBody, instance);
const doCreateMessage = async (convId: number) => {
const replyToIds = await this.getReplyToIds(messageBody, instance);
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
const message = await client.messages.create({
accountId: this.provider.accountId,
conversationId: conversationId,
data: {
content: content,
message_type: messageType,
attachments: attachments,
private: privateMessage || false,
source_id: sourceId,
content_attributes: {
...replyToIds,
const message = await client.messages.create({
accountId: this.provider.accountId,
conversationId: convId,
data: {
content: content,
message_type: messageType,
attachments: attachments,
private: privateMessage || false,
source_id: sourceId,
content_attributes: {
...replyToIds,
},
source_reply_id: sourceReplyId ? sourceReplyId.toString() : null,
},
source_reply_id: sourceReplyId ? sourceReplyId.toString() : null,
},
});
});
if (!message) {
this.logger.warn('message not found');
return null;
if (!message) {
this.logger.warn('message not found');
return null;
}
return message;
};
try {
return await doCreateMessage(conversationId);
} catch (error) {
return this.handleStaleConversationError(
error,
instance,
conversationId,
messageBody,
messageBodyForRetry,
'createMessage',
(newConvId) => doCreateMessage(newConvId),
);
}
}
return message;
private async handleStaleConversationError(
error: any,
instance: InstanceDto,
conversationId: number,
messageBody: any,
messageBodyForRetry: any,
functionName: string,
originalFunction: (newConversationId: number) => Promise<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(
@ -1006,6 +1102,7 @@ export class ChatwootService {
messageBody?: any,
sourceId?: string,
quotedMsg?: MessageModel,
messageBodyForRetry?: any,
) {
if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
@ -1016,54 +1113,65 @@ export class ChatwootService {
}
}
}
const data = new FormData();
const doSendData = async (convId: number) => {
const data = new FormData();
if (content) {
data.append('content', content);
}
data.append('message_type', messageType);
data.append('attachments[]', fileData, { filename: fileName });
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
if (messageBody && instance) {
const replyToIds = await this.getReplyToIds(messageBody, instance);
if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) {
const content = JSON.stringify({
...replyToIds,
});
data.append('content_attributes', content);
if (content) {
data.append('content', content);
}
}
if (sourceReplyId) {
data.append('source_reply_id', sourceReplyId.toString());
}
data.append('message_type', messageType);
if (sourceId) {
data.append('source_id', sourceId);
}
data.append('attachments[]', fileStream, { filename: fileName });
const config = {
method: 'post',
maxBodyLength: Infinity,
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`,
headers: {
api_access_token: this.provider.token,
...data.getHeaders(),
},
data: data,
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
if (messageBody && instance) {
const replyToIds = await this.getReplyToIds(messageBody, instance);
if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) {
const content = JSON.stringify({
...replyToIds,
});
data.append('content_attributes', content);
}
}
if (sourceReplyId) {
data.append('source_reply_id', sourceReplyId.toString());
}
if (sourceId) {
data.append('source_id', sourceId);
}
const config = {
method: 'post',
maxBodyLength: Infinity,
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${convId}/messages`,
headers: {
api_access_token: this.provider.token,
...data.getHeaders(),
},
data: data,
};
const { data: responseData } = await axios.request(config);
return responseData;
};
try {
const { data } = await axios.request(config);
return data;
return await doSendData(conversationId);
} catch (error) {
this.logger.error(error);
return this.handleStaleConversationError(
error,
instance,
conversationId,
messageBody,
messageBodyForRetry,
'sendData',
(newConvId) => doSendData(newConvId),
);
}
}
@ -2304,6 +2412,7 @@ export class ChatwootService {
body,
'WAID:' + body.key.id,
quotedMsg,
null,
);
if (!send) {
@ -2323,6 +2432,7 @@ export class ChatwootService {
body,
'WAID:' + body.key.id,
quotedMsg,
null,
);
if (!send) {
@ -2348,6 +2458,7 @@ export class ChatwootService {
},
'WAID:' + body.key.id,
quotedMsg,
body,
);
if (!send) {
this.logger.warn('message not sent');
@ -2399,6 +2510,8 @@ export class ChatwootService {
instance,
body,
'WAID:' + body.key.id,
quotedMsg,
null,
);
if (!send) {
@ -2440,6 +2553,7 @@ export class ChatwootService {
body,
'WAID:' + body.key.id,
quotedMsg,
null,
);
if (!send) {
@ -2459,6 +2573,7 @@ export class ChatwootService {
body,
'WAID:' + body.key.id,
quotedMsg,
null,
);
if (!send) {
@ -2587,6 +2702,7 @@ export class ChatwootService {
},
'WAID:' + body.key.id,
null,
body,
);
if (!send) {
@ -2673,15 +2789,30 @@ export class ChatwootService {
await this.createBotMessage(instance, msgStatus, 'incoming');
}
if (event === 'connection.update') {
if (body.status === 'open') {
// if we have qrcode count then we understand that a new connection was established
if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) {
const msgConnection = i18next.t('cw.inbox.connected');
await this.createBotMessage(instance, msgConnection, 'incoming');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
chatwootImport.clearAll(instance);
}
if (event === 'connection.update' && body.status === 'open') {
const waInstance = this.waMonitor.waInstances[instance.instanceName];
if (!waInstance) return;
const now = Date.now();
const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0);
// Se a conexão foi estabelecida via QR code, notifica imediatamente.
if (waInstance.qrCode && waInstance.qrCode.count > 0) {
const msgConnection = i18next.t('cw.inbox.connected');
await this.createBotMessage(instance, msgConnection, 'incoming');
waInstance.qrCode.count = 0;
waInstance.lastConnectionNotification = now;
chatwootImport.clearAll(instance);
}
// Se não foi via QR code, verifica o throttling.
else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) {
const msgConnection = i18next.t('cw.inbox.connected');
await this.createBotMessage(instance, msgConnection, 'incoming');
waInstance.lastConnectionNotification = now;
} else {
this.logger.warn(
`Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`,
);
}
}
@ -2861,7 +2992,8 @@ export class ChatwootService {
and created_at >= now() - interval '6h'
order by created_at desc`;
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
const pgClient = await this.getPgClient();
const messagesData = (await pgClient.query(sqlMessages))?.rows;
const ids: string[] = messagesData
.filter((message) => !!message.source_id)
.map((message) => message.source_id.replace('WAID:', ''));

View File

@ -23,15 +23,17 @@ const configService: ConfigService = new ConfigService();
const resources: any = {};
languages.forEach((language) => {
const languagePath = path.join(translationsPath, `${language}.json`);
if (fs.existsSync(languagePath)) {
const translationContent = fs.readFileSync(languagePath, 'utf8');
resources[language] = {
translation: JSON.parse(translationContent),
};
}
});
if (translationsPath) {
languages.forEach((language) => {
const languagePath = path.join(translationsPath, `${language}.json`);
if (fs.existsSync(languagePath)) {
const translationContent = fs.readFileSync(languagePath, 'utf8');
resources[language] = {
translation: JSON.parse(translationContent),
};
}
});
}
i18next.init({
resources,