mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-20 12:22:21 -06:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48fbc3a4e | ||
|
|
cdf06666a1 | ||
|
|
5254928887 | ||
|
|
8468690d37 | ||
|
|
bdd9257c47 | ||
|
|
d6834c8741 | ||
|
|
164beddb39 | ||
|
|
4991f1dc37 | ||
|
|
1b1e3b3e9d | ||
|
|
563ca2dd22 | ||
|
|
4e44bfb222 | ||
|
|
9edd600513 | ||
|
|
501b06d133 | ||
|
|
dc530285d5 | ||
|
|
8775cdf036 | ||
|
|
6ad33df879 | ||
|
|
633d0b4c45 | ||
|
|
82c0eadf7c | ||
|
|
1756abf1e6 | ||
|
|
a2f48030dc | ||
|
|
3214a9fb5b | ||
|
|
4b89e3f987 | ||
|
|
72622dca31 | ||
|
|
d73b72b67e | ||
|
|
20eef33df3 | ||
|
|
37571c03b4 | ||
|
|
017949458b | ||
|
|
2feaf1c74e | ||
|
|
4b043cb4b8 | ||
|
|
b0d261b305 | ||
|
|
c041986e26 | ||
|
|
0976109d27 | ||
|
|
b808dda33b | ||
|
|
98b7f15a43 | ||
|
|
94ddc0dfbe | ||
|
|
d4b0cfd2ba | ||
|
|
a5a46dc72a | ||
|
|
e13434804c | ||
|
|
53cd7d5d13 | ||
|
|
bedfb019aa | ||
|
|
6e1d027750 | ||
|
|
fb1fa4d91a | ||
|
|
57ea6707bc | ||
|
|
ad8df44236 | ||
|
|
c132379b3a | ||
|
|
f7862637b1 | ||
|
|
0d8e8bc0fb | ||
|
|
b62917e80f | ||
|
|
eeb324227b | ||
|
|
c31b62fb3d | ||
|
|
22465c0a56 | ||
|
|
da6f1bd540 | ||
|
|
069786b9fe | ||
|
|
bd0c43feac | ||
|
|
5dc1d02d0a | ||
|
|
8697329f71 | ||
|
|
58b5561f72 | ||
|
|
093515555d | ||
|
|
d8268b0eb1 | ||
|
|
4585850741 | ||
|
|
6c150eed6d |
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:
|
||||||
|
|||||||
2
.github/workflows/check_code_quality.yml
vendored
2
.github/workflows/check_code_quality.yml
vendored
@@ -13,6 +13,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v5
|
||||||
|
|||||||
2
.github/workflows/publish_docker_image.yml
vendored
2
.github/workflows/publish_docker_image.yml
vendored
@@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
4
.github/workflows/security.yml
vendored
4
.github/workflows/security.yml
vendored
@@ -26,6 +26,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
@@ -47,5 +49,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v4
|
uses: actions/dependency-review-action@v4
|
||||||
|
|||||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,3 +1,44 @@
|
|||||||
|
# 2.3.5 (2025-10-15)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Chatwoot Enhancements**: Comprehensive improvements to message handling, editing, deletion and i18n
|
||||||
|
* **Participants Data**: Add participantsData field maintaining backward compatibility for group participants
|
||||||
|
* **LID to Phone Number**: Convert LID to phoneNumber on group participants
|
||||||
|
* **Docker Configurations**: Add Kafka and frontend services to Docker configurations
|
||||||
|
|
||||||
|
### 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.5 with compatibility fixes
|
||||||
|
- Fixed assertSessions signature compatibility using type assertion
|
||||||
|
- Fixed incompatibility in voice call (wavoip) with new Baileys version
|
||||||
|
- Handle undefined status in update by defaulting to 'DELETED'
|
||||||
|
* **Chatwoot Improvements**: Multiple fixes for enhanced reliability
|
||||||
|
- Correct chatId extraction for non-group JIDs
|
||||||
|
- Resolve webhook timeout on deletion with 5+ images
|
||||||
|
- Improve error handling in Chatwoot messages
|
||||||
|
- Adjust conversation verification logic and cache
|
||||||
|
- Optimize conversation reopening logic and connection notification
|
||||||
|
- Fix conversation reopening and connection loop
|
||||||
|
* **Baileys Message Handling**: Enhanced message processing
|
||||||
|
- Add warning log for messages not found
|
||||||
|
- Fix message verification in Baileys service
|
||||||
|
- Simplify linkPreview handling in BaileysStartupService
|
||||||
|
* **Media Validation**: Fix media content validation
|
||||||
|
* **PostgreSQL Connection**: Refactor connection with PostgreSQL and improve message handling
|
||||||
|
|
||||||
|
### Code Quality & Refactoring
|
||||||
|
|
||||||
|
* **Exponential Backoff**: Implement exponential backoff patterns and extract magic numbers to constants
|
||||||
|
* **TypeScript Build**: Update TypeScript build process and dependencies
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
Submodule evolution-manager-v2 updated: fcb38dd407...f054b9bc28
BIN
manager/dist/assets/images/evolution-logo.png
vendored
BIN
manager/dist/assets/images/evolution-logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 25 KiB |
485
manager/dist/assets/index-CO3NSIFj.js
vendored
Normal file
485
manager/dist/assets/index-CO3NSIFj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
461
manager/dist/assets/index-DJ2Q5K8k.js
vendored
461
manager/dist/assets/index-DJ2Q5K8k.js
vendored
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DsIrum0U.css
vendored
Normal file
1
manager/dist/assets/index-DsIrum0U.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DxAxQfZR.css
vendored
1
manager/dist/assets/index-DxAxQfZR.css
vendored
File diff suppressed because one or more lines are too long
4
manager/dist/index.html
vendored
4
manager/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Evolution Manager</title>
|
<title>Evolution Manager</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DJ2Q5K8k.js"></script>
|
<script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DxAxQfZR.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
8
manager_install.sh
Executable file
8
manager_install.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
cd evolution-manager-v2
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
rm -rf manager/dist
|
||||||
|
cp -r evolution-manager-v2/dist manager/dist
|
||||||
5922
package-lock.json
generated
5922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
"src/**/*.ts": [
|
"src/**/*.ts": [
|
||||||
"sh -c 'npm run build'"
|
"sh -c 'tsc --noEmit'"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Events, wa } from '@api/types/wa.types';
|
|||||||
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
||||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||||
import { createJid } from '@utils/createJid';
|
import { createJid } from '@utils/createJid';
|
||||||
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { isBase64, isURL } from 'class-validator';
|
import { isBase64, isURL } from 'class-validator';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
@@ -171,6 +172,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.log(messageRaw);
|
||||||
|
|
||||||
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusine
|
|||||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||||
import { createJid } from '@utils/createJid';
|
import { createJid } from '@utils/createJid';
|
||||||
import { status } from '@utils/renderStatus';
|
import { status } from '@utils/renderStatus';
|
||||||
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { arrayUnique, isURL } from 'class-validator';
|
import { arrayUnique, isURL } from 'class-validator';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
@@ -655,6 +656,8 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.log(messageRaw);
|
||||||
|
|
||||||
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
|||||||
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
||||||
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
||||||
import { status } from '@utils/renderStatus';
|
import { status } from '@utils/renderStatus';
|
||||||
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma';
|
import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma';
|
||||||
import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files';
|
import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files';
|
||||||
import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db';
|
import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db';
|
||||||
@@ -152,13 +153,7 @@ import { v4 } from 'uuid';
|
|||||||
import { BaileysMessageProcessor } from './baileysMessage.processor';
|
import { BaileysMessageProcessor } from './baileysMessage.processor';
|
||||||
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
||||||
|
|
||||||
export interface ExtendedMessageKey extends WAMessageKey {
|
|
||||||
senderPn?: string;
|
|
||||||
previousRemoteJid?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExtendedIMessageKey extends proto.IMessageKey {
|
export interface ExtendedIMessageKey extends proto.IMessageKey {
|
||||||
senderPn?: string;
|
|
||||||
remoteJidAlt?: string;
|
remoteJidAlt?: string;
|
||||||
participantAlt?: string;
|
participantAlt?: string;
|
||||||
server_id?: string;
|
server_id?: string;
|
||||||
@@ -254,6 +249,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
private endSession = false;
|
private endSession = false;
|
||||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||||
|
|
||||||
|
// Cache TTL constants (in seconds)
|
||||||
|
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
|
||||||
|
private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates
|
||||||
|
|
||||||
public stateConnection: wa.StateConnection = { state: 'close' };
|
public stateConnection: wa.StateConnection = { state: 'close' };
|
||||||
|
|
||||||
public phoneNumber: string;
|
public phoneNumber: string;
|
||||||
@@ -500,8 +499,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
try {
|
try {
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
const webMessageInfo = (await this.prismaRepository.$queryRaw`
|
const webMessageInfo = (await this.prismaRepository.$queryRaw`
|
||||||
SELECT * FROM "Message"
|
SELECT * FROM "Message"
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`) as proto.IWebMessageInfo[];
|
`) as proto.IWebMessageInfo[];
|
||||||
|
|
||||||
@@ -1000,10 +999,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m.key.remoteJid?.includes('@lid') && (m.key as ExtendedIMessageKey).senderPn) {
|
|
||||||
m.key.remoteJid = (m.key as ExtendedIMessageKey).senderPn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Long.isLong(m?.messageTimestamp)) {
|
if (Long.isLong(m?.messageTimestamp)) {
|
||||||
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
||||||
}
|
}
|
||||||
@@ -1066,10 +1061,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
for (const received of messages) {
|
for (const received of messages) {
|
||||||
if (received.key.remoteJid?.includes('@lid') && (received.key as ExtendedMessageKey).senderPn) {
|
|
||||||
(received.key as ExtendedMessageKey).previousRemoteJid = received.key.remoteJid;
|
|
||||||
received.key.remoteJid = (received.key as ExtendedMessageKey).senderPn;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
received?.messageStubParameters?.some?.((param) =>
|
received?.messageStubParameters?.some?.((param) =>
|
||||||
[
|
[
|
||||||
@@ -1117,9 +1108,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
||||||
const oldMessage = await this.getMessage(editedMessage.key, true);
|
const oldMessage = await this.getMessage(editedMessage.key, true);
|
||||||
if ((oldMessage as any)?.id) {
|
if ((oldMessage as any)?.id) {
|
||||||
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
|
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
|
||||||
? Math.floor(editedMessage.timestampMs.toNumber() / 1000)
|
? Math.floor(received?.messageTimestamp.toNumber())
|
||||||
: Math.floor((editedMessage.timestampMs as number) / 1000);
|
: Math.floor(received?.messageTimestamp as number);
|
||||||
|
|
||||||
await this.prismaRepository.message.update({
|
await this.prismaRepository.message.update({
|
||||||
where: { id: (oldMessage as any).id },
|
where: { id: (oldMessage as any).id },
|
||||||
@@ -1150,7 +1141,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(type !== 'notify' && type !== 'append') ||
|
(type !== 'notify' && type !== 'append') ||
|
||||||
@@ -1270,7 +1261,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
} else {
|
} else {
|
||||||
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
||||||
}
|
}
|
||||||
@@ -1358,12 +1349,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
|
|
||||||
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.log(messageRaw);
|
||||||
|
|
||||||
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
@@ -1437,9 +1426,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.remoteJid?.includes('@lid') && key.remoteJidAlt) {
|
if (update.message !== null && update.status === undefined) continue;
|
||||||
key.remoteJid = key.remoteJidAlt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||||
|
|
||||||
@@ -1480,7 +1467,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
keyId: key.id,
|
keyId: key.id,
|
||||||
remoteJid: key?.remoteJid,
|
remoteJid: key?.remoteJid,
|
||||||
fromMe: key.fromMe,
|
fromMe: key.fromMe,
|
||||||
participant: key?.remoteJid,
|
participant: key?.participant,
|
||||||
status: status[update.status] ?? 'DELETED',
|
status: status[update.status] ?? 'DELETED',
|
||||||
pollUpdates,
|
pollUpdates,
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
@@ -1491,14 +1478,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
const messages = (await this.prismaRepository.$queryRaw`
|
const messages = (await this.prismaRepository.$queryRaw`
|
||||||
SELECT * FROM "Message"
|
SELECT * FROM "Message"
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`) 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) {
|
||||||
@@ -1533,7 +1524,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (status[update.status] === status[4]) {
|
if (status[update.status] === status[4]) {
|
||||||
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
|
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
|
||||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaRepository.message.update({
|
await this.prismaRepository.message.update({
|
||||||
@@ -1591,12 +1582,66 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
'group-participants.update': (participantsUpdate: {
|
'group-participants.update': async (participantsUpdate: {
|
||||||
id: string;
|
id: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
action: ParticipantAction;
|
action: ParticipantAction;
|
||||||
}) => {
|
}) => {
|
||||||
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate);
|
// ENHANCEMENT: Adds participantsData field while maintaining backward compatibility
|
||||||
|
// MAINTAINS: participants: string[] (original JID strings)
|
||||||
|
// ADDS: participantsData: { jid: string, phoneNumber: string, name?: string, imgUrl?: string }[]
|
||||||
|
// This enables LID to phoneNumber conversion without breaking existing webhook consumers
|
||||||
|
|
||||||
|
// Helper to normalize participantId as phone number
|
||||||
|
const normalizePhoneNumber = (id: string): string => {
|
||||||
|
// Remove @lid, @s.whatsapp.net suffixes and extract just the number part
|
||||||
|
return id.split('@')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Usa o mesmo método que o endpoint /group/participants
|
||||||
|
const groupParticipants = await this.findParticipants({ groupJid: participantsUpdate.id });
|
||||||
|
|
||||||
|
// Validação para garantir que temos dados válidos
|
||||||
|
if (!groupParticipants?.participants || !Array.isArray(groupParticipants.participants)) {
|
||||||
|
throw new Error('Invalid participant data received from findParticipants');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtra apenas os participantes que estão no evento
|
||||||
|
const resolvedParticipants = participantsUpdate.participants.map((participantId) => {
|
||||||
|
const participantData = groupParticipants.participants.find((p) => p.id === participantId);
|
||||||
|
|
||||||
|
let phoneNumber: string;
|
||||||
|
if (participantData?.phoneNumber) {
|
||||||
|
phoneNumber = participantData.phoneNumber;
|
||||||
|
} else {
|
||||||
|
phoneNumber = normalizePhoneNumber(participantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jid: participantId,
|
||||||
|
phoneNumber,
|
||||||
|
name: participantData?.name,
|
||||||
|
imgUrl: participantData?.imgUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mantém formato original + adiciona dados resolvidos
|
||||||
|
const enhancedParticipantsUpdate = {
|
||||||
|
...participantsUpdate,
|
||||||
|
participants: participantsUpdate.participants, // Mantém array original de strings
|
||||||
|
// Adiciona dados resolvidos em campo separado
|
||||||
|
participantsData: resolvedParticipants,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to resolve participant data for GROUP_PARTICIPANTS_UPDATE webhook: ${error.message} | Group: ${participantsUpdate.id} | Participants: ${participantsUpdate.participants.length}`,
|
||||||
|
);
|
||||||
|
// Fallback - envia sem conversão
|
||||||
|
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateGroupMetadataCache(participantsUpdate.id);
|
this.updateGroupMetadataCache(participantsUpdate.id);
|
||||||
},
|
},
|
||||||
@@ -1678,6 +1723,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') {
|
if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') {
|
||||||
|
if (call.from.endsWith('@lid')) {
|
||||||
|
call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string);
|
||||||
|
}
|
||||||
const msg = await this.client.sendMessage(call.from, { text: settings.msgCall });
|
const msg = await this.client.sendMessage(call.from, { text: settings.msgCall });
|
||||||
|
|
||||||
this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' });
|
this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' });
|
||||||
@@ -3367,18 +3415,13 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numberJid = numberVerified?.jid || user.jid;
|
const numberJid = numberVerified?.jid || user.jid;
|
||||||
const lid =
|
|
||||||
typeof numberVerified?.lid === 'string'
|
|
||||||
? numberVerified.lid
|
|
||||||
: numberJid.includes('@lid')
|
|
||||||
? numberJid.split('@')[1]
|
|
||||||
: 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,
|
undefined,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -3530,7 +3573,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
keyId: messageId,
|
keyId: messageId,
|
||||||
remoteJid: response.key.remoteJid,
|
remoteJid: response.key.remoteJid,
|
||||||
fromMe: response.key.fromMe,
|
fromMe: response.key.fromMe,
|
||||||
participant: response.key?.remoteJid,
|
participant: response.key?.participant,
|
||||||
status: 'DELETED',
|
status: 'DELETED',
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
};
|
};
|
||||||
@@ -3590,7 +3633,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
|
if (
|
||||||
|
Object.keys(msg.message).length === 1 &&
|
||||||
|
Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo')
|
||||||
|
) {
|
||||||
throw 'The message is messageContextInfo';
|
throw 'The message is messageContextInfo';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3965,7 +4011,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
keyId: messageId,
|
keyId: messageId,
|
||||||
remoteJid: messageSent.key.remoteJid,
|
remoteJid: messageSent.key.remoteJid,
|
||||||
fromMe: messageSent.key.fromMe,
|
fromMe: messageSent.key.fromMe,
|
||||||
participant: messageSent.key?.remoteJid,
|
participant: messageSent.key?.participant,
|
||||||
status: 'EDITED',
|
status: 'EDITED',
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
};
|
};
|
||||||
@@ -4461,7 +4507,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
const result = await this.prismaRepository.$executeRaw`
|
const result = await this.prismaRepository.$executeRaw`
|
||||||
UPDATE "Message"
|
UPDATE "Message"
|
||||||
SET "status" = ${status[4]}
|
SET "status" = ${status[4]}
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
AND "key"->>'remoteJid' = ${remoteJid}
|
AND "key"->>'remoteJid' = ${remoteJid}
|
||||||
@@ -4486,7 +4532,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.prismaRepository.chat.findFirst({ where: { remoteJid } }),
|
this.prismaRepository.chat.findFirst({ where: { remoteJid } }),
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
this.prismaRepository.$queryRaw`
|
this.prismaRepository.$queryRaw`
|
||||||
SELECT COUNT(*)::int as count FROM "Message"
|
SELECT COUNT(*)::int as count FROM "Message"
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
AND "key"->>'remoteJid' = ${remoteJid}
|
AND "key"->>'remoteJid' = ${remoteJid}
|
||||||
AND ("key"->>'fromMe')::boolean = false
|
AND ("key"->>'fromMe')::boolean = false
|
||||||
@@ -4561,8 +4607,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;
|
||||||
}
|
}
|
||||||
@@ -4766,7 +4812,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||||
keyFilters?.senderPn ? { key: { path: ['senderPn'], equals: keyFilters?.senderPn } } : {},
|
keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -4796,7 +4842,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||||
keyFilters?.senderPn ? { key: { path: ['senderPn'], equals: keyFilters?.senderPn } } : {},
|
keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
||||||
import { ExtendedMessageKey } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service';
|
|
||||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||||
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||||||
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
||||||
@@ -24,7 +23,7 @@ import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageM
|
|||||||
import i18next from '@utils/i18n';
|
import i18next from '@utils/i18n';
|
||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { proto } from 'baileys';
|
import { proto, WAMessageKey } from 'baileys';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import { Jimp, JimpMime } from 'jimp';
|
import { Jimp, JimpMime } from 'jimp';
|
||||||
@@ -33,6 +32,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;
|
||||||
@@ -44,6 +45,25 @@ interface ChatwootMessage {
|
|||||||
export class ChatwootService {
|
export class ChatwootService {
|
||||||
private readonly logger = new Logger('ChatwootService');
|
private readonly logger = new Logger('ChatwootService');
|
||||||
|
|
||||||
|
// HTTP timeout constants
|
||||||
|
private readonly MEDIA_DOWNLOAD_TIMEOUT_MS = 60000; // 60 seconds for large files
|
||||||
|
|
||||||
|
// S3/MinIO retry configuration (external storage - longer delays, fewer retries)
|
||||||
|
private readonly S3_MAX_RETRIES = 3;
|
||||||
|
private readonly S3_BASE_DELAY_MS = 1000; // Base delay: 1 second
|
||||||
|
private readonly S3_MAX_DELAY_MS = 8000; // Max delay: 8 seconds
|
||||||
|
|
||||||
|
// Database polling retry configuration (internal DB - shorter delays, more retries)
|
||||||
|
private readonly DB_POLLING_MAX_RETRIES = 5;
|
||||||
|
private readonly DB_POLLING_BASE_DELAY_MS = 100; // Base delay: 100ms
|
||||||
|
private readonly DB_POLLING_MAX_DELAY_MS = 2000; // Max delay: 2 seconds
|
||||||
|
|
||||||
|
// Webhook processing delay
|
||||||
|
private readonly WEBHOOK_INITIAL_DELAY_MS = 500; // Initial delay before processing webhook
|
||||||
|
|
||||||
|
// Lock polling delay
|
||||||
|
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||||
|
|
||||||
private provider: any;
|
private provider: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -568,27 +588,29 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createConversation(instance: InstanceDto, body: any) {
|
public async createConversation(instance: InstanceDto, body: any) {
|
||||||
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
|
const isLid = body.key.addressingMode === 'lid' && body.key.remoteJidAlt;
|
||||||
const remoteJid = body.key.remoteJid;
|
const remoteJid = isLid ? body.key.remoteJidAlt : body.key.remoteJid;
|
||||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||||
const maxWaitTime = 5000; // 5 secounds
|
const maxWaitTime = 5000; // 5 seconds
|
||||||
|
const client = await this.clientCw(instance);
|
||||||
|
if (!client) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Processa atualização de contatos já criados @lid
|
// Processa atualização de contatos já criados @lid
|
||||||
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
|
if (isLid && body.key.remoteJidAlt !== body.key.remoteJid) {
|
||||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
||||||
if (contact && contact.identifier !== body.key.senderPn) {
|
if (contact && contact.identifier !== body.key.remoteJidAlt) {
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
|
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.remoteJidAlt: ${body.key.remoteJidAlt}`,
|
||||||
);
|
);
|
||||||
const updateContact = await this.updateContact(instance, contact.id, {
|
const updateContact = await this.updateContact(instance, contact.id, {
|
||||||
identifier: body.key.senderPn,
|
identifier: body.key.remoteJidAlt,
|
||||||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
phone_number: `+${body.key.remoteJidAlt.split('@')[0]}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateContact === null) {
|
if (updateContact === null) {
|
||||||
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
|
const baseContact = await this.findContact(instance, body.key.remoteJidAlt.split('@')[0]);
|
||||||
if (baseContact) {
|
if (baseContact) {
|
||||||
await this.mergeContacts(baseContact.id, contact.id);
|
await this.mergeContacts(baseContact.id, contact.id);
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
@@ -605,6 +627,22 @@ export class ChatwootService {
|
|||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||||
|
let conversationExists: conversation | boolean;
|
||||||
|
try {
|
||||||
|
conversationExists = await client.conversations.get({
|
||||||
|
accountId: this.provider.accountId,
|
||||||
|
conversationId: conversationId,
|
||||||
|
});
|
||||||
|
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error getting conversation: ${error}`);
|
||||||
|
conversationExists = false;
|
||||||
|
}
|
||||||
|
if (!conversationExists) {
|
||||||
|
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
||||||
|
this.cache.delete(cacheKey);
|
||||||
|
return await this.createConversation(instance, body);
|
||||||
|
}
|
||||||
return conversationId;
|
return conversationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +655,7 @@ export class ChatwootService {
|
|||||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await new Promise((res) => setTimeout(res, 300));
|
await new Promise((res) => setTimeout(res, this.LOCK_POLLING_DELAY_MS));
|
||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||||
@@ -639,11 +677,8 @@ export class ChatwootService {
|
|||||||
return (await this.cache.get(cacheKey)) as number;
|
return (await this.cache.get(cacheKey)) as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await this.clientCw(instance);
|
|
||||||
if (!client) return null;
|
|
||||||
|
|
||||||
const isGroup = remoteJid.includes('@g.us');
|
const isGroup = remoteJid.includes('@g.us');
|
||||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0].split(':')[0];
|
||||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||||
const filterInbox = await this.getInbox(instance);
|
const filterInbox = await this.getInbox(instance);
|
||||||
if (!filterInbox) return null;
|
if (!filterInbox) return null;
|
||||||
@@ -769,7 +804,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (inboxConversation) {
|
if (inboxConversation) {
|
||||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||||
this.cache.set(cacheKey, inboxConversation.id);
|
this.cache.set(cacheKey, inboxConversation.id, 8 * 3600);
|
||||||
return inboxConversation.id;
|
return inboxConversation.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,7 +837,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||||||
this.cache.set(cacheKey, conversation.id);
|
this.cache.set(cacheKey, conversation.id, 8 * 3600);
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
} finally {
|
} finally {
|
||||||
await this.cache.delete(lockKey);
|
await this.cache.delete(lockKey);
|
||||||
@@ -1123,20 +1158,140 @@ export class ChatwootService {
|
|||||||
|
|
||||||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||||||
try {
|
try {
|
||||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
|
||||||
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
// URLs presigned podem expirar, então convertemos para base64
|
||||||
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
let mediaBuffer: Buffer;
|
||||||
|
let mimeType: string;
|
||||||
|
let fileName: string;
|
||||||
|
|
||||||
if (!mimeType) {
|
try {
|
||||||
const parts = media.split('/');
|
this.logger.verbose(`Downloading media from: ${media}`);
|
||||||
fileName = decodeURIComponent(parts[parts.length - 1]);
|
|
||||||
|
|
||||||
|
// Tentar fazer download do arquivo com autenticação do Chatwoot
|
||||||
|
// maxRedirects: 0 para não seguir redirects automaticamente
|
||||||
const response = await axios.get(media, {
|
const response = await axios.get(media, {
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
|
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
||||||
|
headers: {
|
||||||
|
api_access_token: this.provider.token,
|
||||||
|
},
|
||||||
|
maxRedirects: 0, // Não seguir redirects automaticamente
|
||||||
|
validateStatus: (status) => status < 500, // Aceitar redirects (301, 302, 307)
|
||||||
});
|
});
|
||||||
mimeType = response.headers['content-type'];
|
|
||||||
|
this.logger.verbose(`Initial response status: ${response.status}`);
|
||||||
|
|
||||||
|
// Se for redirect, pegar a URL de destino e fazer novo request
|
||||||
|
if (response.status >= 300 && response.status < 400) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
this.logger.verbose(`Redirect to: ${redirectUrl}`);
|
||||||
|
|
||||||
|
if (redirectUrl) {
|
||||||
|
// Fazer novo request para a URL do S3/MinIO (sem autenticação, pois é presigned URL)
|
||||||
|
// IMPORTANTE: Chatwoot pode gerar a URL presigned ANTES de fazer upload
|
||||||
|
// Vamos tentar com retry usando exponential backoff se receber 404 (arquivo ainda não disponível)
|
||||||
|
this.logger.verbose('Downloading from S3/MinIO...');
|
||||||
|
|
||||||
|
let s3Response;
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = this.S3_MAX_RETRIES;
|
||||||
|
const baseDelay = this.S3_BASE_DELAY_MS;
|
||||||
|
const maxDelay = this.S3_MAX_DELAY_MS;
|
||||||
|
|
||||||
|
while (retryCount <= maxRetries) {
|
||||||
|
s3Response = await axios.get(redirectUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
||||||
|
validateStatus: (status) => status < 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.verbose(
|
||||||
|
`S3 response status: ${s3Response.status}, size: ${s3Response.data?.byteLength || 0} bytes (attempt ${retryCount + 1}/${maxRetries + 1})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Se não for 404, sair do loop
|
||||||
|
if (s3Response.status !== 404) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se for 404 e ainda tem tentativas, aguardar com exponential backoff e tentar novamente
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
// Exponential backoff com max delay (seguindo padrão do webhook controller)
|
||||||
|
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
||||||
|
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
||||||
|
this.logger.warn(
|
||||||
|
`File not yet available in S3/MinIO (attempt ${retryCount + 1}/${maxRetries + 1}). Retrying in ${backoffDelay}ms with exponential backoff...`,
|
||||||
|
);
|
||||||
|
this.logger.verbose(`MinIO Response: ${errorBody}`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
||||||
|
retryCount++;
|
||||||
|
} else {
|
||||||
|
// Última tentativa falhou
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Após todas as tentativas, verificar o status final
|
||||||
|
if (s3Response.status === 404) {
|
||||||
|
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
||||||
|
this.logger.error(`File not found in S3/MinIO after ${maxRetries + 1} attempts. URL: ${redirectUrl}`);
|
||||||
|
this.logger.error(`MinIO Error Response: ${errorBody}`);
|
||||||
|
throw new Error(
|
||||||
|
'File not found in S3/MinIO (404). The file may have been deleted, the URL is incorrect, or Chatwoot has not finished uploading yet.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s3Response.status === 403) {
|
||||||
|
this.logger.error(`Access denied to S3/MinIO. URL may have expired: ${redirectUrl}`);
|
||||||
|
throw new Error(
|
||||||
|
'Access denied to S3/MinIO (403). Presigned URL may have expired. Check S3_PRESIGNED_EXPIRATION setting.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s3Response.status >= 400) {
|
||||||
|
this.logger.error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
||||||
|
throw new Error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaBuffer = Buffer.from(s3Response.data);
|
||||||
|
mimeType = s3Response.headers['content-type'] || 'application/octet-stream';
|
||||||
|
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes from S3, type: ${mimeType}`);
|
||||||
|
} else {
|
||||||
|
this.logger.error('Redirect response without Location header');
|
||||||
|
throw new Error('Redirect without Location header');
|
||||||
|
}
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
this.logger.error(`File not found (404) at: ${media}`);
|
||||||
|
throw new Error('File not found (404). The attachment may not exist in Chatwoot storage.');
|
||||||
|
} else if (response.status >= 400) {
|
||||||
|
this.logger.error(`HTTP ${response.status}: ${response.statusText} for URL: ${media}`);
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
} else {
|
||||||
|
// Download direto sem redirect
|
||||||
|
mediaBuffer = Buffer.from(response.data);
|
||||||
|
mimeType = response.headers['content-type'] || 'application/octet-stream';
|
||||||
|
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes directly, type: ${mimeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrair nome do arquivo da URL ou usar o content-disposition
|
||||||
|
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||||
|
if (parsedMedia?.name && parsedMedia?.ext) {
|
||||||
|
fileName = parsedMedia.name + parsedMedia.ext;
|
||||||
|
} else {
|
||||||
|
const parts = media.split('/');
|
||||||
|
fileName = decodeURIComponent(parts[parts.length - 1].split('?')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(`File name: ${fileName}, size: ${mediaBuffer.length} bytes`);
|
||||||
|
} catch (downloadError) {
|
||||||
|
this.logger.error('[MEDIA DOWNLOAD] ❌ Error downloading media from: ' + media);
|
||||||
|
this.logger.error(`[MEDIA DOWNLOAD] Error message: ${downloadError.message}`);
|
||||||
|
this.logger.error(`[MEDIA DOWNLOAD] Error stack: ${downloadError.stack}`);
|
||||||
|
this.logger.error(`[MEDIA DOWNLOAD] Full error: ${JSON.stringify(downloadError, null, 2)}`);
|
||||||
|
throw new Error(`Failed to download media: ${downloadError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determinar o tipo de mídia pelo mimetype
|
||||||
let type = 'document';
|
let type = 'document';
|
||||||
|
|
||||||
switch (mimeType.split('/')[0]) {
|
switch (mimeType.split('/')[0]) {
|
||||||
@@ -1154,10 +1309,12 @@ export class ChatwootService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Para áudio, usar base64 com data URI
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
|
const base64Audio = `data:${mimeType};base64,${mediaBuffer.toString('base64')}`;
|
||||||
const data: SendAudioDto = {
|
const data: SendAudioDto = {
|
||||||
number: number,
|
number: number,
|
||||||
audio: media,
|
audio: base64Audio,
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@@ -1169,8 +1326,12 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
|
||||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
const base64Media = mediaBuffer.toString('base64');
|
||||||
|
|
||||||
|
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||||
|
const parsedExt = path.parse(fileName)?.ext;
|
||||||
|
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
|
||||||
type = 'document';
|
type = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1339,7 @@ export class ChatwootService {
|
|||||||
number: number,
|
number: number,
|
||||||
mediatype: type as any,
|
mediatype: type as any,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
media: media,
|
media: base64Media, // Base64 puro, sem prefixo
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@@ -1194,6 +1355,7 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
|
throw error; // Re-throw para que o erro seja tratado pelo caller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1233,9 +1395,87 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processa deleção de mensagem em background
|
||||||
|
* Método assíncrono chamado via setImmediate para não bloquear resposta do webhook
|
||||||
|
*/
|
||||||
|
private async processDeletion(instance: InstanceDto, body: any, deleteLockKey: string) {
|
||||||
|
this.logger.warn(`[DELETE] 🗑️ Processing deletion - messageId: ${body.id}`);
|
||||||
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
|
|
||||||
|
// Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos)
|
||||||
|
const messages = await this.prismaRepository.message.findMany({
|
||||||
|
where: {
|
||||||
|
chatwootMessageId: body.id,
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (messages && messages.length > 0) {
|
||||||
|
this.logger.warn(`[DELETE] Found ${messages.length} message(s) to delete from Chatwoot message ${body.id}`);
|
||||||
|
this.logger.verbose(`[DELETE] Messages keys: ${messages.map((m) => (m.key as any)?.id).join(', ')}`);
|
||||||
|
|
||||||
|
// Deletar cada mensagem no WhatsApp
|
||||||
|
for (const message of messages) {
|
||||||
|
const key = message.key as WAMessageKey;
|
||||||
|
this.logger.warn(
|
||||||
|
`[DELETE] Attempting to delete WhatsApp message - keyId: ${key?.id}, remoteJid: ${key?.remoteJid}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
||||||
|
this.logger.warn(`[DELETE] ✅ Message ${key.id} deleted in WhatsApp successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[DELETE] ❌ Error deleting message ${key.id} in WhatsApp: ${error}`);
|
||||||
|
this.logger.error(`[DELETE] Error details: ${JSON.stringify(error, null, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover todas as mensagens do banco de dados
|
||||||
|
await this.prismaRepository.message.deleteMany({
|
||||||
|
where: {
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
chatwootMessageId: body.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.warn(`[DELETE] ✅ SUCCESS: ${messages.length} message(s) deleted from WhatsApp and database`);
|
||||||
|
} else {
|
||||||
|
// Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
|
||||||
|
this.logger.warn(`[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${body.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liberar lock após processar
|
||||||
|
await this.cache.delete(deleteLockKey);
|
||||||
|
}
|
||||||
|
|
||||||
public async receiveWebhook(instance: InstanceDto, body: any) {
|
public async receiveWebhook(instance: InstanceDto, body: any) {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
|
||||||
|
// para evitar race condition com webhooks duplicados
|
||||||
|
let isDeletionEvent = false;
|
||||||
|
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||||
|
isDeletionEvent = true;
|
||||||
|
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
||||||
|
|
||||||
|
// Verificar se já está processando esta deleção
|
||||||
|
if (await this.cache.has(deleteLockKey)) {
|
||||||
|
this.logger.warn(`[DELETE] ⏭️ SKIPPING: Deletion already in progress for messageId: ${body.id}`);
|
||||||
|
return { message: 'already_processing' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adquirir lock IMEDIATAMENTE por 30 segundos
|
||||||
|
await this.cache.set(deleteLockKey, true, 30);
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`[WEBHOOK-DELETE] Event: ${body.event}, messageId: ${body.id}, conversation: ${body.conversation?.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para deleções, processar IMEDIATAMENTE (sem delay)
|
||||||
|
// Para outros eventos, aguardar delay inicial
|
||||||
|
if (!isDeletionEvent) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, this.WEBHOOK_INITIAL_DELAY_MS));
|
||||||
|
}
|
||||||
|
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@@ -1254,6 +1494,39 @@ export class ChatwootService {
|
|||||||
this.cache.delete(keyToDelete);
|
this.cache.delete(keyToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log para debug de mensagens deletadas
|
||||||
|
if (body.event === 'message_updated') {
|
||||||
|
this.logger.verbose(
|
||||||
|
`Message updated event - deleted: ${body.content_attributes?.deleted}, messageId: ${body.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processar deleção de mensagem ANTES das outras validações
|
||||||
|
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||||
|
// Lock já foi adquirido no início do método (antes do delay)
|
||||||
|
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
||||||
|
|
||||||
|
// ESTRATÉGIA: Processar em background e responder IMEDIATAMENTE
|
||||||
|
// Isso evita timeout do Chatwoot (5s) quando há muitas imagens (> 5s de processamento)
|
||||||
|
this.logger.warn(`[DELETE] 🚀 Starting background deletion - messageId: ${body.id}`);
|
||||||
|
|
||||||
|
// Executar em background (sem await) - não bloqueia resposta do webhook
|
||||||
|
setImmediate(async () => {
|
||||||
|
try {
|
||||||
|
await this.processDeletion(instance, body, deleteLockKey);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[DELETE] ❌ Background deletion failed for messageId ${body.id}: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RESPONDER IMEDIATAMENTE ao Chatwoot (< 50ms)
|
||||||
|
return {
|
||||||
|
message: 'deletion_accepted',
|
||||||
|
messageId: body.id,
|
||||||
|
note: 'Deletion is being processed in background',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!body?.conversation ||
|
!body?.conversation ||
|
||||||
body.private ||
|
body.private ||
|
||||||
@@ -1285,7 +1558,7 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
||||||
|
|
||||||
@@ -1370,7 +1643,10 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
|
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
|
||||||
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
|
if (
|
||||||
|
body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' &&
|
||||||
|
body?.conversation?.messages[0]?.id === body?.id
|
||||||
|
) {
|
||||||
return { message: 'bot' };
|
return { message: 'bot' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,40 +1670,58 @@ export class ChatwootService {
|
|||||||
|
|
||||||
for (const message of body.conversation.messages) {
|
for (const message of body.conversation.messages) {
|
||||||
if (message.attachments && message.attachments.length > 0) {
|
if (message.attachments && message.attachments.length > 0) {
|
||||||
for (const attachment of message.attachments) {
|
// Processa anexos de forma assíncrona para não bloquear o webhook
|
||||||
if (!messageReceived) {
|
const processAttachments = async () => {
|
||||||
formatText = null;
|
for (const attachment of message.attachments) {
|
||||||
|
if (!messageReceived) {
|
||||||
|
formatText = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: Options = {
|
||||||
|
quoted: await this.getQuotedMessage(body, instance),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageSent = await this.sendAttachment(
|
||||||
|
waInstance,
|
||||||
|
chatId,
|
||||||
|
attachment.data_url,
|
||||||
|
formatText,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messageSent && body.conversation?.id) {
|
||||||
|
this.onSendMessageError(instance, body.conversation?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageSent) {
|
||||||
|
await this.updateChatwootMessageId(
|
||||||
|
{
|
||||||
|
...messageSent,
|
||||||
|
owner: instance.instanceName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
messageId: body.id,
|
||||||
|
inboxId: body.inbox?.id,
|
||||||
|
conversationId: body.conversation?.id,
|
||||||
|
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
||||||
|
},
|
||||||
|
instance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
if (body.conversation?.id) {
|
||||||
|
this.onSendMessageError(instance, body.conversation?.id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const options: Options = {
|
// Executa em background sem bloquear
|
||||||
quoted: await this.getQuotedMessage(body, instance),
|
processAttachments().catch((error) => {
|
||||||
};
|
this.logger.error(error);
|
||||||
|
});
|
||||||
const messageSent = await this.sendAttachment(
|
|
||||||
waInstance,
|
|
||||||
chatId,
|
|
||||||
attachment.data_url,
|
|
||||||
formatText,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
if (!messageSent && body.conversation?.id) {
|
|
||||||
this.onSendMessageError(instance, body.conversation?.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.updateChatwootMessageId(
|
|
||||||
{
|
|
||||||
...messageSent,
|
|
||||||
owner: instance.instanceName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: body.id,
|
|
||||||
inboxId: body.inbox?.id,
|
|
||||||
conversationId: body.conversation?.id,
|
|
||||||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
|
||||||
},
|
|
||||||
instance,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const data: SendTextDto = {
|
const data: SendTextDto = {
|
||||||
number: chatId,
|
number: chatId,
|
||||||
@@ -1450,10 +1744,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
{
|
messageSent, // Já tem instanceId
|
||||||
...messageSent,
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
inboxId: body.inbox?.id,
|
inboxId: body.inbox?.id,
|
||||||
@@ -1483,7 +1774,7 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (lastMessage && !lastMessage.chatwootIsRead) {
|
if (lastMessage && !lastMessage.chatwootIsRead) {
|
||||||
const key = lastMessage.key as ExtendedMessageKey;
|
const key = lastMessage.key as WAMessageKey;
|
||||||
|
|
||||||
waInstance?.markMessageAsRead({
|
waInstance?.markMessageAsRead({
|
||||||
readMessages: [
|
readMessages: [
|
||||||
@@ -1541,14 +1832,63 @@ export class ChatwootService {
|
|||||||
chatwootMessageIds: ChatwootMessage,
|
chatwootMessageIds: ChatwootMessage,
|
||||||
instance: InstanceDto,
|
instance: InstanceDto,
|
||||||
) {
|
) {
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
if (!chatwootMessageIds.messageId || !key?.id) {
|
||||||
|
this.logger.verbose(
|
||||||
|
`Skipping updateChatwootMessageId - messageId: ${chatwootMessageIds.messageId}, keyId: ${key?.id}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use instanceId from message or fallback to instance
|
||||||
|
const instanceId = message.instanceId || instance.instanceId;
|
||||||
|
|
||||||
|
this.logger.verbose(
|
||||||
|
`Updating message with chatwootMessageId: ${chatwootMessageIds.messageId}, keyId: ${key.id}, instanceId: ${instanceId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verifica se a mensagem existe antes de atualizar usando polling com exponential backoff
|
||||||
|
let retries = 0;
|
||||||
|
const maxRetries = this.DB_POLLING_MAX_RETRIES;
|
||||||
|
const baseDelay = this.DB_POLLING_BASE_DELAY_MS;
|
||||||
|
const maxDelay = this.DB_POLLING_MAX_DELAY_MS;
|
||||||
|
let messageExists = false;
|
||||||
|
|
||||||
|
while (retries < maxRetries && !messageExists) {
|
||||||
|
const existingMessage = await this.prismaRepository.message.findFirst({
|
||||||
|
where: {
|
||||||
|
instanceId: instanceId,
|
||||||
|
key: {
|
||||||
|
path: ['id'],
|
||||||
|
equals: key.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMessage) {
|
||||||
|
messageExists = true;
|
||||||
|
this.logger.verbose(`Message found in database after ${retries} retries`);
|
||||||
|
} else {
|
||||||
|
retries++;
|
||||||
|
if (retries < maxRetries) {
|
||||||
|
// Exponential backoff com max delay (seguindo padrão do sistema)
|
||||||
|
const backoffDelay = Math.min(baseDelay * Math.pow(2, retries - 1), maxDelay);
|
||||||
|
this.logger.verbose(`Message not found, retry ${retries}/${maxRetries} in ${backoffDelay}ms`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
||||||
|
} else {
|
||||||
|
this.logger.verbose(`Message not found after ${retries} attempts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageExists) {
|
||||||
|
this.logger.warn(`Message not found in database after ${maxRetries} retries, keyId: ${key.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
await this.prismaRepository.$executeRaw`
|
const result = await this.prismaRepository.$executeRaw`
|
||||||
UPDATE "Message"
|
UPDATE "Message"
|
||||||
SET
|
SET
|
||||||
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
||||||
@@ -1556,10 +1896,12 @@ export class ChatwootService {
|
|||||||
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
||||||
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
||||||
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
||||||
WHERE "instanceId" = ${instance.instanceId}
|
WHERE "instanceId" = ${instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
this.logger.verbose(`Update result: ${result} rows affected`);
|
||||||
|
|
||||||
if (this.isImportHistoryAvailable()) {
|
if (this.isImportHistoryAvailable()) {
|
||||||
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||||||
}
|
}
|
||||||
@@ -1609,7 +1951,7 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = message?.key as ExtendedMessageKey;
|
const key = message?.key as WAMessageKey;
|
||||||
|
|
||||||
if (message && key?.id) {
|
if (message && key?.id) {
|
||||||
return {
|
return {
|
||||||
@@ -1913,6 +2255,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.upsert' || event === 'send.message') {
|
if (event === 'messages.upsert' || event === 'send.message') {
|
||||||
|
this.logger.info(`[${event}] New message received - Instance: ${JSON.stringify(body, null, 2)}`);
|
||||||
if (body.key.remoteJid === 'status@broadcast') {
|
if (body.key.remoteJid === 'status@broadcast') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2235,9 +2578,8 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||||
const editedText = `${
|
const editedMessageContent =
|
||||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
|
||||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
|
||||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -2245,11 +2587,14 @@ export class ChatwootService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
||||||
|
|
||||||
if (message && message.chatwootConversationId) {
|
if (message && message.chatwootConversationId && message.chatwootMessageId) {
|
||||||
|
// Criar nova mensagem com formato: "Mensagem editada:\n\nteste1"
|
||||||
|
const editedText = `\n\n\`${i18next.t('cw.message.edited')}:\`\n\n${editedMessageContent}`;
|
||||||
|
|
||||||
const send = await this.createMessage(
|
const send = await this.createMessage(
|
||||||
instance,
|
instance,
|
||||||
message.chatwootConversationId,
|
message.chatwootConversationId,
|
||||||
@@ -2327,15 +2672,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 msgConnection = i18next.t('cw.inbox.connected');
|
const now = Date.now();
|
||||||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0);
|
||||||
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
|
|
||||||
chatwootImport.clearAll(instance);
|
// 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)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,12 +112,19 @@ class ChatwootImport {
|
|||||||
const bindInsert = [provider.accountId];
|
const bindInsert = [provider.accountId];
|
||||||
|
|
||||||
for (const contact of contactsChunk) {
|
for (const contact of contactsChunk) {
|
||||||
bindInsert.push(contact.pushName);
|
const isGroup = this.isIgnorePhoneNumber(contact.remoteJid);
|
||||||
|
|
||||||
|
const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName;
|
||||||
|
bindInsert.push(contactName);
|
||||||
const bindName = `$${bindInsert.length}`;
|
const bindName = `$${bindInsert.length}`;
|
||||||
|
|
||||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
let bindPhoneNumber: string;
|
||||||
const bindPhoneNumber = `$${bindInsert.length}`;
|
if (!isGroup) {
|
||||||
|
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||||
|
bindPhoneNumber = `$${bindInsert.length}`;
|
||||||
|
} else {
|
||||||
|
bindPhoneNumber = 'NULL';
|
||||||
|
}
|
||||||
bindInsert.push(contact.remoteJid);
|
bindInsert.push(contact.remoteJid);
|
||||||
const bindIdentifier = `$${bindInsert.length}`;
|
const bindIdentifier = `$${bindInsert.length}`;
|
||||||
|
|
||||||
|
|||||||
@@ -826,7 +826,7 @@ export class ChannelStartupService {
|
|||||||
const msg = message.message;
|
const msg = message.message;
|
||||||
|
|
||||||
// Se só tem messageContextInfo, não é mídia válida
|
// Se só tem messageContextInfo, não é mídia válida
|
||||||
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
|
if (Object.keys(msg).length === 1 && Object.prototype.hasOwnProperty.call(msg, 'messageContextInfo')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import fs from 'fs';
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const __dirname = path.resolve(process.cwd(), 'src', 'utils');
|
|
||||||
|
|
||||||
const languages = ['en', 'pt-BR', 'es'];
|
const languages = ['en', 'pt-BR', 'es'];
|
||||||
const translationsPath = path.join(__dirname, 'translations');
|
const translationsPath = path.join(__dirname, 'translations');
|
||||||
const configService: ConfigService = new ConfigService();
|
const configService: ConfigService = new ConfigService();
|
||||||
|
|||||||
Reference in New Issue
Block a user