mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 09:59:40 -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: |
|
||||
- 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:
|
||||
|
||||
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)
|
||||
|
||||
### 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:
|
||||
evolution_v2:
|
||||
image: evoapicloud/evolution-api:v2.3.1
|
||||
image: evoapicloud/evolution-api:v2.3.5
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
42
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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:', ''));
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user