mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-19 03:42:23 -06:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3454bec79f | ||
|
|
8c27f11f5b | ||
|
|
ae9f3efeff | ||
|
|
ba3a2fae59 | ||
|
|
aa0d793d26 | ||
|
|
48bda1b5af | ||
|
|
dd21a29ea6 | ||
|
|
e83a7e2e88 | ||
|
|
d58d0b8bff | ||
|
|
4efc9b65bc | ||
|
|
cd71ff503d | ||
|
|
582166e5ae | ||
|
|
e1ae03c1e4 | ||
|
|
0737c45df2 | ||
|
|
adbe1079d5 | ||
|
|
423f629b04 | ||
|
|
946dcaeb2e | ||
|
|
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: |
|
||||
- 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.6]
|
||||
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
|
||||
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
||||
validations:
|
||||
|
||||
2
.github/workflows/check_code_quality.yml
vendored
2
.github/workflows/check_code_quality.yml
vendored
@@ -13,6 +13,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Node
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
@@ -15,6 +15,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
@@ -15,6 +15,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
||||
4
.github/workflows/security.yml
vendored
4
.github/workflows/security.yml
vendored
@@ -26,6 +26,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
@@ -47,5 +49,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,3 +1,72 @@
|
||||
# 2.3.6 (2025-10-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **Baileys, Chatwoot, OnWhatsapp Cache**: Multiple implementations and fixes
|
||||
- Fixed cache for PN, LID and g.us numbers to send correct number
|
||||
- Fixed audio and document sending via Chatwoot in Baileys channel
|
||||
- Multiple fixes in Chatwoot integration
|
||||
- Fixed ignored messages when receiving leads
|
||||
|
||||
### Fixed
|
||||
|
||||
* **Baileys**: Fix buffer storage in database
|
||||
- Correctly save Uint8Array values to database
|
||||
* **Baileys**: Simplify logging of messageSent object
|
||||
- Fixed "this.isZero not is function" error
|
||||
|
||||
### Chore
|
||||
|
||||
* **Version**: Bump version to 2.3.6 and update Baileys dependency to 7.0.0-rc.6
|
||||
* **Workflows**: Update checkout step to include submodules
|
||||
- Added 'submodules: recursive' option to checkout step in multiple workflow files to ensure submodules are properly initialized during CI/CD processes
|
||||
* **Manager**: Update asset files and install process
|
||||
- Updated subproject reference in evolution-manager-v2 to the latest commit
|
||||
- Enhanced the manager_install.sh script to include npm install and build steps
|
||||
- Replaced old JavaScript asset file with a new version for improved performance
|
||||
- Added a new CSS file for consistent styling across the application
|
||||
|
||||
# 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)
|
||||
|
||||
### 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.6
|
||||
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
|
||||
|
||||
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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Evolution Manager</title>
|
||||
<script type="module" crossorigin src="/assets/index-DJ2Q5K8k.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DxAxQfZR.css">
|
||||
<script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
|
||||
</head>
|
||||
<body>
|
||||
<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
|
||||
4330
package-lock.json
generated
4330
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.3.4",
|
||||
"version": "2.3.6",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
@@ -56,7 +56,7 @@
|
||||
"eslint --fix"
|
||||
],
|
||||
"src/**/*.ts": [
|
||||
"sh -c 'npm run build'"
|
||||
"sh -c 'tsc --noEmit'"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
@@ -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.6",
|
||||
"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;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Events, wa } from '@api/types/wa.types';
|
||||
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
@@ -171,6 +172,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
|
||||
await chatbotController.emit({
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusine
|
||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { arrayUnique, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
@@ -655,6 +656,8 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
|
||||
await chatbotController.emit({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BaileysEventMap, MessageUpsertType, proto } from 'baileys';
|
||||
import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys';
|
||||
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
|
||||
|
||||
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
||||
@@ -12,7 +12,7 @@ export class BaileysMessageProcessor {
|
||||
private subscription?: Subscription;
|
||||
|
||||
protected messageSubject = new Subject<{
|
||||
messages: proto.IWebMessageInfo[];
|
||||
messages: WAMessage[];
|
||||
type: MessageUpsertType;
|
||||
requestId?: string;
|
||||
settings: any;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
||||
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
||||
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma';
|
||||
import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files';
|
||||
import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db';
|
||||
@@ -132,7 +133,6 @@ import { Label } from 'baileys/lib/Types/Label';
|
||||
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
||||
import { spawn } from 'child_process';
|
||||
import { isArray, isBase64, isURL } from 'class-validator';
|
||||
import { randomBytes } from 'crypto';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import FormData from 'form-data';
|
||||
@@ -152,13 +152,7 @@ import { v4 } from 'uuid';
|
||||
import { BaileysMessageProcessor } from './baileysMessage.processor';
|
||||
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
||||
|
||||
export interface ExtendedMessageKey extends WAMessageKey {
|
||||
senderPn?: string;
|
||||
previousRemoteJid?: string | null;
|
||||
}
|
||||
|
||||
export interface ExtendedIMessageKey extends proto.IMessageKey {
|
||||
senderPn?: string;
|
||||
remoteJidAlt?: string;
|
||||
participantAlt?: string;
|
||||
server_id?: string;
|
||||
@@ -254,6 +248,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
private endSession = false;
|
||||
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 phoneNumber: string;
|
||||
@@ -500,8 +498,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
try {
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
const webMessageInfo = (await this.prismaRepository.$queryRaw`
|
||||
SELECT * FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
SELECT * FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
AND "key"->>'id' = ${key.id}
|
||||
`) as proto.IWebMessageInfo[];
|
||||
|
||||
@@ -877,6 +875,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
||||
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
|
||||
for await (const contact of contacts) {
|
||||
this.logger.debug(`Updating contact: ${JSON.stringify(contact, null, 2)}`);
|
||||
contactsRaw.push({
|
||||
remoteJid: contact.id,
|
||||
pushName: contact?.name ?? contact?.verifiedName,
|
||||
@@ -896,10 +895,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
);
|
||||
await this.prismaRepository.$transaction(updateTransactions);
|
||||
|
||||
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
||||
if (usersContacts) {
|
||||
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
||||
}
|
||||
//const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1000,10 +996,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
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)) {
|
||||
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
||||
}
|
||||
@@ -1066,10 +1058,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
) => {
|
||||
try {
|
||||
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 (
|
||||
received?.messageStubParameters?.some?.((param) =>
|
||||
[
|
||||
@@ -1117,9 +1105,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
||||
const oldMessage = await this.getMessage(editedMessage.key, true);
|
||||
if ((oldMessage as any)?.id) {
|
||||
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
|
||||
? Math.floor(editedMessage.timestampMs.toNumber() / 1000)
|
||||
: Math.floor((editedMessage.timestampMs as number) / 1000);
|
||||
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
|
||||
? Math.floor(received?.messageTimestamp.toNumber())
|
||||
: Math.floor(received?.messageTimestamp as number);
|
||||
|
||||
await this.prismaRepository.message.update({
|
||||
where: { id: (oldMessage as any).id },
|
||||
@@ -1145,12 +1133,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const messageKey = `${this.instance.id}_${received.key.id}`;
|
||||
const cached = await this.baileysCache.get(messageKey);
|
||||
|
||||
if (cached && !editedMessage) {
|
||||
if (cached && !editedMessage && !requestId) {
|
||||
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
||||
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||
|
||||
if (
|
||||
(type !== 'notify' && type !== 'append') ||
|
||||
@@ -1270,7 +1258,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
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 {
|
||||
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
||||
}
|
||||
@@ -1358,11 +1346,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
|
||||
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
|
||||
}
|
||||
this.logger.verbose(messageRaw);
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
|
||||
@@ -1377,7 +1363,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
|
||||
});
|
||||
|
||||
const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = {
|
||||
const contactRaw: {
|
||||
remoteJid: string;
|
||||
pushName: string;
|
||||
profilePicUrl?: string;
|
||||
instanceId: string;
|
||||
} = {
|
||||
remoteJid: received.key.remoteJid,
|
||||
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
||||
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
||||
@@ -1388,6 +1379,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contactRaw.remoteJid.includes('@s.whatsapp') || contactRaw.remoteJid.includes('@lid')) {
|
||||
await saveOnWhatsappCache([
|
||||
{
|
||||
remoteJid:
|
||||
messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid,
|
||||
remoteJidAlt: messageRaw.key.remoteJidAlt,
|
||||
lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
||||
|
||||
@@ -1417,10 +1419,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
update: contactRaw,
|
||||
create: contactRaw,
|
||||
});
|
||||
|
||||
if (contactRaw.remoteJid.includes('@s.whatsapp')) {
|
||||
await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@@ -1428,7 +1426,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
},
|
||||
|
||||
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
|
||||
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
||||
this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
||||
|
||||
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
||||
|
||||
@@ -1437,9 +1435,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.remoteJid?.includes('@lid') && key.remoteJidAlt) {
|
||||
key.remoteJid = key.remoteJidAlt;
|
||||
}
|
||||
if (update.message !== null && update.status === undefined) continue;
|
||||
|
||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||
|
||||
@@ -1480,7 +1476,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
keyId: key.id,
|
||||
remoteJid: key?.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.remoteJid,
|
||||
participant: key?.participant,
|
||||
status: status[update.status] ?? 'DELETED',
|
||||
pollUpdates,
|
||||
instanceId: this.instanceId,
|
||||
@@ -1491,14 +1487,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
const messages = (await this.prismaRepository.$queryRaw`
|
||||
SELECT * FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
SELECT * FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
AND "key"->>'id' = ${key.id}
|
||||
LIMIT 1
|
||||
`) 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) {
|
||||
@@ -1533,7 +1533,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (status[update.status] === status[4]) {
|
||||
this.logger.log(`Update as read in message.update ${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({
|
||||
@@ -1591,12 +1591,66 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
});
|
||||
},
|
||||
|
||||
'group-participants.update': (participantsUpdate: {
|
||||
'group-participants.update': async (participantsUpdate: {
|
||||
id: string;
|
||||
participants: string[];
|
||||
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);
|
||||
},
|
||||
@@ -1678,6 +1732,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' });
|
||||
@@ -1750,7 +1807,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if (events['group-participants.update']) {
|
||||
const payload = events['group-participants.update'];
|
||||
const payload = events['group-participants.update'] as any;
|
||||
this.groupHandler['group-participants.update'](payload);
|
||||
}
|
||||
}
|
||||
@@ -1918,6 +1975,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
quoted: any,
|
||||
messageId?: string,
|
||||
ephemeralExpiration?: number,
|
||||
contextInfo?: any,
|
||||
// participants?: GroupParticipant[],
|
||||
) {
|
||||
sender = sender.toLowerCase();
|
||||
@@ -1934,8 +1992,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
|
||||
|
||||
// NOTE: NÃO DEVEMOS GERAR O messageId AQUI, SOMENTE SE VIER INFORMADO POR PARAMETRO. A GERAÇÃO ANTERIOR IMPEDE O WZAP DE IDENTIFICAR A SOURCE.
|
||||
if (messageId) option.messageId = messageId;
|
||||
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
|
||||
|
||||
if (message['viewOnceMessage']) {
|
||||
const m = generateWAMessageFromContent(sender, message, {
|
||||
@@ -1972,10 +2030,19 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (contextInfo) {
|
||||
message['contextInfo'] = contextInfo;
|
||||
}
|
||||
|
||||
if (message['conversation']) {
|
||||
return await this.client.sendMessage(
|
||||
sender,
|
||||
{ text: message['conversation'], mentions, linkPreview: linkPreview } as unknown as AnyMessageContent,
|
||||
{
|
||||
text: message['conversation'],
|
||||
mentions,
|
||||
linkPreview: linkPreview,
|
||||
contextInfo: message['contextInfo'],
|
||||
} as unknown as AnyMessageContent,
|
||||
option as unknown as MiscMessageGenerationOptions,
|
||||
);
|
||||
}
|
||||
@@ -1983,7 +2050,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
||||
return await this.client.sendMessage(
|
||||
sender,
|
||||
{ forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message }, mentions },
|
||||
{
|
||||
forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message },
|
||||
mentions,
|
||||
contextInfo: message['contextInfo'],
|
||||
},
|
||||
option as unknown as MiscMessageGenerationOptions,
|
||||
);
|
||||
}
|
||||
@@ -2114,7 +2185,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (options?.quoted) {
|
||||
const m = options?.quoted;
|
||||
|
||||
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo);
|
||||
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as WAMessage);
|
||||
|
||||
if (msg) {
|
||||
quoted = msg;
|
||||
@@ -2124,6 +2195,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
let messageSent: WAMessage;
|
||||
|
||||
let mentions: string[];
|
||||
let contextInfo: any;
|
||||
|
||||
if (isJidGroup(sender)) {
|
||||
let group;
|
||||
try {
|
||||
@@ -2162,7 +2235,27 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
// group?.participants,
|
||||
);
|
||||
} else {
|
||||
messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted);
|
||||
contextInfo = {
|
||||
mentionedJid: [],
|
||||
groupMentions: [],
|
||||
//expiration: 7776000,
|
||||
ephemeralSettingTimestamp: {
|
||||
low: Math.floor(Date.now() / 1000) - 172800,
|
||||
high: 0,
|
||||
unsigned: false,
|
||||
},
|
||||
disappearingMode: { initiator: 0 },
|
||||
};
|
||||
messageSent = await this.sendMessage(
|
||||
sender,
|
||||
message,
|
||||
mentions,
|
||||
linkPreview,
|
||||
quoted,
|
||||
null,
|
||||
undefined,
|
||||
contextInfo,
|
||||
);
|
||||
}
|
||||
|
||||
if (Long.isLong(messageSent?.messageTimestamp)) {
|
||||
@@ -2282,7 +2375,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
this.logger.verbose(messageSent);
|
||||
|
||||
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
||||
|
||||
@@ -3289,125 +3382,128 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
|
||||
});
|
||||
|
||||
// Separate @lid numbers from normal numbers
|
||||
const lidUsers = jids.users.filter(({ jid }) => jid.includes('@lid'));
|
||||
const normalUsers = jids.users.filter(({ jid }) => !jid.includes('@lid'));
|
||||
// Unified cache verification for all numbers (normal and LID)
|
||||
const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', ''));
|
||||
|
||||
// For normal numbers, use traditional Baileys verification
|
||||
let normalVerifiedUsers: OnWhatsAppDto[] = [];
|
||||
if (normalUsers.length > 0) {
|
||||
console.log('normalUsers', normalUsers);
|
||||
const numbersToVerify = normalUsers.map(({ jid }) => jid.replace('+', ''));
|
||||
console.log('numbersToVerify', numbersToVerify);
|
||||
// Get all numbers from cache
|
||||
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
||||
|
||||
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
||||
console.log('cachedNumbers', cachedNumbers);
|
||||
// Separate numbers that are and are not in cache
|
||||
const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions));
|
||||
const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid));
|
||||
|
||||
const filteredNumbers = numbersToVerify.filter(
|
||||
(jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)),
|
||||
);
|
||||
console.log('filteredNumbers', filteredNumbers);
|
||||
// Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache
|
||||
let verify: { jid: string; exists: boolean }[] = [];
|
||||
const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid'));
|
||||
|
||||
const verify = await this.client.onWhatsApp(...filteredNumbers);
|
||||
console.log('verify', verify);
|
||||
normalVerifiedUsers = await Promise.all(
|
||||
normalUsers.map(async (user) => {
|
||||
let numberVerified: (typeof verify)[0] | null = null;
|
||||
|
||||
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||
if (cached) {
|
||||
return new OnWhatsAppDto(
|
||||
cached.remoteJid,
|
||||
true,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
|
||||
cached.lid || (cached.remoteJid.includes('@lid') ? cached.remoteJid.split('@')[1] : undefined),
|
||||
);
|
||||
}
|
||||
|
||||
// Brazilian numbers
|
||||
if (user.number.startsWith('55')) {
|
||||
const numberWithDigit =
|
||||
user.number.slice(4, 5) === '9' && user.number.length === 13
|
||||
? user.number
|
||||
: `${user.number.slice(0, 4)}9${user.number.slice(4)}`;
|
||||
const numberWithoutDigit =
|
||||
user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5);
|
||||
|
||||
numberVerified = verify.find(
|
||||
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
||||
);
|
||||
}
|
||||
|
||||
// Mexican/Argentina numbers
|
||||
// Ref: https://faq.whatsapp.com/1294841057948784
|
||||
if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) {
|
||||
let prefix = '';
|
||||
if (user.number.startsWith('52')) {
|
||||
prefix = '1';
|
||||
}
|
||||
if (user.number.startsWith('54')) {
|
||||
prefix = '9';
|
||||
}
|
||||
|
||||
const numberWithDigit =
|
||||
user.number.slice(2, 3) === prefix && user.number.length === 13
|
||||
? user.number
|
||||
: `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`;
|
||||
const numberWithoutDigit =
|
||||
user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3);
|
||||
|
||||
numberVerified = verify.find(
|
||||
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!numberVerified) {
|
||||
numberVerified = verify.find((v) => v.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(
|
||||
numberJid,
|
||||
!!numberVerified?.exists,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
||||
lid,
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (normalNumbersNotInCache.length > 0) {
|
||||
this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`);
|
||||
verify = await this.client.onWhatsApp(...normalNumbersNotInCache);
|
||||
}
|
||||
|
||||
// For @lid numbers, always consider them as valid
|
||||
const lidVerifiedUsers: OnWhatsAppDto[] = lidUsers.map((user) => {
|
||||
return new OnWhatsAppDto(
|
||||
user.jid,
|
||||
true,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
|
||||
user.jid.split('@')[1],
|
||||
);
|
||||
});
|
||||
const verifiedUsers = await Promise.all(
|
||||
jids.users.map(async (user) => {
|
||||
// Try to get from cache first (works for all: normal and LID)
|
||||
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||
|
||||
if (cached) {
|
||||
this.logger.verbose(`Number ${user.number} found in cache`);
|
||||
return new OnWhatsAppDto(
|
||||
cached.remoteJid,
|
||||
true,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
|
||||
cached.lid || (cached.remoteJid.includes('@lid') ? 'lid' : undefined),
|
||||
);
|
||||
}
|
||||
|
||||
// If it's a LID number and not in cache, consider it valid
|
||||
if (user.jid.includes('@lid')) {
|
||||
return new OnWhatsAppDto(
|
||||
user.jid,
|
||||
true,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
|
||||
'lid',
|
||||
);
|
||||
}
|
||||
|
||||
// If not in cache and is a normal number, use Baileys verification
|
||||
let numberVerified: (typeof verify)[0] | null = null;
|
||||
|
||||
// Brazilian numbers
|
||||
if (user.number.startsWith('55')) {
|
||||
const numberWithDigit =
|
||||
user.number.slice(4, 5) === '9' && user.number.length === 13
|
||||
? user.number
|
||||
: `${user.number.slice(0, 4)}9${user.number.slice(4)}`;
|
||||
const numberWithoutDigit =
|
||||
user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5);
|
||||
|
||||
numberVerified = verify.find(
|
||||
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
||||
);
|
||||
}
|
||||
|
||||
// Mexican/Argentina numbers
|
||||
// Ref: https://faq.whatsapp.com/1294841057948784
|
||||
if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) {
|
||||
let prefix = '';
|
||||
if (user.number.startsWith('52')) {
|
||||
prefix = '1';
|
||||
}
|
||||
if (user.number.startsWith('54')) {
|
||||
prefix = '9';
|
||||
}
|
||||
|
||||
const numberWithDigit =
|
||||
user.number.slice(2, 3) === prefix && user.number.length === 13
|
||||
? user.number
|
||||
: `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`;
|
||||
const numberWithoutDigit =
|
||||
user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3);
|
||||
|
||||
numberVerified = verify.find(
|
||||
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!numberVerified) {
|
||||
numberVerified = verify.find((v) => v.jid === user.jid);
|
||||
}
|
||||
|
||||
const numberJid = numberVerified?.jid || user.jid;
|
||||
|
||||
return new OnWhatsAppDto(
|
||||
numberJid,
|
||||
!!numberVerified?.exists,
|
||||
user.number,
|
||||
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
||||
undefined,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Combine results
|
||||
onWhatsapp.push(...normalVerifiedUsers, ...lidVerifiedUsers);
|
||||
onWhatsapp.push(...verifiedUsers);
|
||||
|
||||
// Save to cache only valid numbers
|
||||
await saveOnWhatsappCache(
|
||||
onWhatsapp
|
||||
.filter((user) => user.exists)
|
||||
.map((user) => ({
|
||||
// TODO: Salvar no cache apenas números que NÃO estavam no cache
|
||||
const numbersToCache = onWhatsapp.filter((user) => {
|
||||
if (!user.exists) return false;
|
||||
// Verifica se estava no cache usando jidOptions
|
||||
const cached = cachedNumbers?.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||
return !cached;
|
||||
});
|
||||
|
||||
if (numbersToCache.length > 0) {
|
||||
this.logger.verbose(`Salvando ${numbersToCache.length} números no cache`);
|
||||
await saveOnWhatsappCache(
|
||||
numbersToCache.map((user) => ({
|
||||
remoteJid: user.jid,
|
||||
jidOptions: user.jid.replace('+', ''),
|
||||
lid: user.lid,
|
||||
lid: user.lid === 'lid' ? 'lid' : undefined,
|
||||
})),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return onWhatsapp;
|
||||
}
|
||||
@@ -3530,7 +3626,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
keyId: messageId,
|
||||
remoteJid: response.key.remoteJid,
|
||||
fromMe: response.key.fromMe,
|
||||
participant: response.key?.remoteJid,
|
||||
participant: response.key?.participant,
|
||||
status: 'DELETED',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
@@ -3965,7 +4061,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
keyId: messageId,
|
||||
remoteJid: messageSent.key.remoteJid,
|
||||
fromMe: messageSent.key.fromMe,
|
||||
participant: messageSent.key?.remoteJid,
|
||||
participant: messageSent.key?.participant,
|
||||
status: 'EDITED',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
@@ -4348,24 +4444,37 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
throw new Error('Method not available in the Baileys service');
|
||||
}
|
||||
|
||||
private convertLongToNumber(obj: any): any {
|
||||
private deserializeMessageBuffers(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Long.isLong(obj)) {
|
||||
return obj.toNumber();
|
||||
if (typeof obj === 'object' && !Array.isArray(obj) && !Buffer.isBuffer(obj)) {
|
||||
const keys = Object.keys(obj);
|
||||
const isIndexedObject = keys.every((key) => !isNaN(Number(key)));
|
||||
|
||||
if (isIndexedObject && keys.length > 0) {
|
||||
const values = keys.sort((a, b) => Number(a) - Number(b)).map((key) => obj[key]);
|
||||
return new Uint8Array(values);
|
||||
}
|
||||
}
|
||||
|
||||
// Is Buffer?, converter to Uint8Array
|
||||
if (Buffer.isBuffer(obj)) {
|
||||
return new Uint8Array(obj);
|
||||
}
|
||||
|
||||
// Process arrays recursively
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.convertLongToNumber(item));
|
||||
return obj.map((item) => this.deserializeMessageBuffers(item));
|
||||
}
|
||||
|
||||
// Process objects recursively
|
||||
if (typeof obj === 'object') {
|
||||
const converted: any = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
converted[key] = this.convertLongToNumber(obj[key]);
|
||||
converted[key] = this.deserializeMessageBuffers(obj[key]);
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
@@ -4386,8 +4495,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
? 'Você'
|
||||
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
||||
status: status[message.status],
|
||||
message: this.convertLongToNumber({ ...message.message }),
|
||||
contextInfo: this.convertLongToNumber(contentMsg?.contextInfo),
|
||||
message: this.deserializeMessageBuffers({ ...message.message }),
|
||||
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),
|
||||
messageType: contentType || 'unknown',
|
||||
messageTimestamp: Long.isLong(message.messageTimestamp)
|
||||
? message.messageTimestamp.toNumber()
|
||||
@@ -4461,7 +4570,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
const result = await this.prismaRepository.$executeRaw`
|
||||
UPDATE "Message"
|
||||
UPDATE "Message"
|
||||
SET "status" = ${status[4]}
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
AND "key"->>'remoteJid' = ${remoteJid}
|
||||
@@ -4486,7 +4595,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
this.prismaRepository.chat.findFirst({ where: { remoteJid } }),
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
this.prismaRepository.$queryRaw`
|
||||
SELECT COUNT(*)::int as count FROM "Message"
|
||||
SELECT COUNT(*)::int as count FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
AND "key"->>'remoteJid' = ${remoteJid}
|
||||
AND ("key"->>'fromMe')::boolean = false
|
||||
@@ -4561,8 +4670,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;
|
||||
}
|
||||
@@ -4766,7 +4875,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
{
|
||||
OR: [
|
||||
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 +4905,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
{
|
||||
OR: [
|
||||
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 { 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 { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||||
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 { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { proto } from 'baileys';
|
||||
import { WAMessageContent, WAMessageKey } from 'baileys';
|
||||
import dayjs from 'dayjs';
|
||||
import FormData from 'form-data';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
@@ -44,6 +43,9 @@ interface ChatwootMessage {
|
||||
export class ChatwootService {
|
||||
private readonly logger = new Logger('ChatwootService');
|
||||
|
||||
// Lock polling delay
|
||||
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||
|
||||
private provider: any;
|
||||
|
||||
constructor(
|
||||
@@ -568,27 +570,31 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
|
||||
const remoteJid = body.key.remoteJid;
|
||||
const isLid = body.key.addressingMode === 'lid';
|
||||
const isGroup = body.key.remoteJid.endsWith('@g.us');
|
||||
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
|
||||
const { remoteJid } = body.key;
|
||||
const cacheKey = `${instance.instanceName}: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 {
|
||||
// Processa atualização de contatos já criados @lid
|
||||
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
|
||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
||||
if (contact && contact.identifier !== body.key.senderPn) {
|
||||
if (phoneNumber && remoteJid && !isGroup) {
|
||||
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||
if (contact && contact.identifier !== remoteJid) {
|
||||
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}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
|
||||
);
|
||||
const updateContact = await this.updateContact(instance, contact.id, {
|
||||
identifier: body.key.senderPn,
|
||||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
||||
identifier: remoteJid,
|
||||
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||
});
|
||||
|
||||
if (updateContact === null) {
|
||||
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
|
||||
const baseContact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||
if (baseContact) {
|
||||
await this.mergeContacts(baseContact.id, contact.id);
|
||||
this.logger.verbose(
|
||||
@@ -604,7 +610,23 @@ export class ChatwootService {
|
||||
// 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}`);
|
||||
this.logger.verbose(`Found conversation to: ${phoneNumber}, 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;
|
||||
}
|
||||
|
||||
@@ -617,7 +639,7 @@ export class ChatwootService {
|
||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||
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)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
@@ -639,11 +661,7 @@ export class ChatwootService {
|
||||
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 chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
||||
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].split(':')[0];
|
||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
if (!filterInbox) return null;
|
||||
@@ -653,14 +671,15 @@ export class ChatwootService {
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
participantJid.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
@@ -673,12 +692,12 @@ export class ChatwootService {
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
participantJid.split('@')[0],
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
participantJid,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -686,6 +705,7 @@ export class ChatwootService {
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
this.logger.verbose(`Searching contact for: ${chatId}`);
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
|
||||
if (contact) {
|
||||
@@ -769,7 +789,7 @@ export class ChatwootService {
|
||||
|
||||
if (inboxConversation) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -802,7 +822,7 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
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;
|
||||
} finally {
|
||||
await this.cache.delete(lockKey);
|
||||
@@ -1158,7 +1178,7 @@ export class ChatwootService {
|
||||
const data: SendAudioDto = {
|
||||
number: number,
|
||||
audio: media,
|
||||
delay: 1200,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
quoted: options?.quoted,
|
||||
};
|
||||
|
||||
@@ -1169,7 +1189,7 @@ export class ChatwootService {
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||
type = 'document';
|
||||
}
|
||||
@@ -1194,6 +1214,7 @@ export class ChatwootService {
|
||||
return messageSent;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw error; // Re-throw para que o erro seja tratado pelo caller
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1275,6 +1296,7 @@ export class ChatwootService {
|
||||
|
||||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
instance.instanceId = waInstance.instanceId;
|
||||
|
||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||
const message = await this.prismaRepository.message.findFirst({
|
||||
@@ -1285,7 +1307,7 @@ export class ChatwootService {
|
||||
});
|
||||
|
||||
if (message) {
|
||||
const key = message.key as ExtendedMessageKey;
|
||||
const key = message.key as WAMessageKey;
|
||||
|
||||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
||||
|
||||
@@ -1370,7 +1392,10 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
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' };
|
||||
}
|
||||
|
||||
@@ -1417,7 +1442,6 @@ export class ChatwootService {
|
||||
await this.updateChatwootMessageId(
|
||||
{
|
||||
...messageSent,
|
||||
owner: instance.instanceName,
|
||||
},
|
||||
{
|
||||
messageId: body.id,
|
||||
@@ -1432,7 +1456,7 @@ export class ChatwootService {
|
||||
const data: SendTextDto = {
|
||||
number: chatId,
|
||||
text: formatText,
|
||||
delay: 1200,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
quoted: await this.getQuotedMessage(body, instance),
|
||||
};
|
||||
|
||||
@@ -1452,7 +1476,6 @@ export class ChatwootService {
|
||||
await this.updateChatwootMessageId(
|
||||
{
|
||||
...messageSent,
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
{
|
||||
messageId: body.id,
|
||||
@@ -1483,7 +1506,7 @@ export class ChatwootService {
|
||||
},
|
||||
});
|
||||
if (lastMessage && !lastMessage.chatwootIsRead) {
|
||||
const key = lastMessage.key as ExtendedMessageKey;
|
||||
const key = lastMessage.key as WAMessageKey;
|
||||
|
||||
waInstance?.markMessageAsRead({
|
||||
readMessages: [
|
||||
@@ -1520,7 +1543,7 @@ export class ChatwootService {
|
||||
const data: SendTextDto = {
|
||||
number: chatId,
|
||||
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
||||
delay: 1200,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
};
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
@@ -1541,14 +1564,14 @@ export class ChatwootService {
|
||||
chatwootMessageIds: ChatwootMessage,
|
||||
instance: InstanceDto,
|
||||
) {
|
||||
const key = message.key as ExtendedMessageKey;
|
||||
const key = message.key as WAMessageKey;
|
||||
|
||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
await this.prismaRepository.$executeRaw`
|
||||
const result = await this.prismaRepository.$executeRaw`
|
||||
UPDATE "Message"
|
||||
SET
|
||||
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
||||
@@ -1560,6 +1583,8 @@ export class ChatwootService {
|
||||
AND "key"->>'id' = ${key.id}
|
||||
`;
|
||||
|
||||
this.logger.verbose(`Update result: ${result} rows affected`);
|
||||
|
||||
if (this.isImportHistoryAvailable()) {
|
||||
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||||
}
|
||||
@@ -1609,12 +1634,13 @@ export class ChatwootService {
|
||||
},
|
||||
});
|
||||
|
||||
const key = message?.key as ExtendedMessageKey;
|
||||
const key = message?.key as WAMessageKey;
|
||||
const messageContent = message?.message as WAMessageContent;
|
||||
|
||||
if (message && key?.id) {
|
||||
if (messageContent && key?.id) {
|
||||
return {
|
||||
key: message.key as proto.IMessageKey,
|
||||
message: message.message as proto.IMessage,
|
||||
key: key,
|
||||
message: messageContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1913,6 +1939,7 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
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') {
|
||||
return;
|
||||
}
|
||||
@@ -2003,7 +2030,10 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const rawPhoneNumber =
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe
|
||||
? body.key.participantAlt.split('@')[0]
|
||||
: body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
@@ -2017,9 +2047,11 @@ export class ChatwootService {
|
||||
let content: string;
|
||||
|
||||
if (!body.key.fromMe) {
|
||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||||
content = bodyMessage
|
||||
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
|
||||
: `**${formattedPhoneNumber} - ${participantName}:**`;
|
||||
} else {
|
||||
content = `${bodyMessage}`;
|
||||
content = bodyMessage || '';
|
||||
}
|
||||
|
||||
const send = await this.sendData(
|
||||
@@ -2144,7 +2176,10 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const rawPhoneNumber =
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe
|
||||
? body.key.participantAlt.split('@')[0]
|
||||
: body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
@@ -2235,9 +2270,8 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||
const editedText = `${
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||||
const editedMessageContent =
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
|
||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||
|
||||
if (!message) {
|
||||
@@ -2245,11 +2279,14 @@ export class ChatwootService {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = message.key as ExtendedMessageKey;
|
||||
const key = message.key as WAMessageKey;
|
||||
|
||||
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(
|
||||
instance,
|
||||
message.chatwootConversationId,
|
||||
@@ -2327,15 +2364,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 >= 30000) {
|
||||
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];
|
||||
|
||||
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}`;
|
||||
|
||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||
const bindPhoneNumber = `$${bindInsert.length}`;
|
||||
|
||||
let bindPhoneNumber: string;
|
||||
if (!isGroup) {
|
||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||
bindPhoneNumber = `$${bindInsert.length}`;
|
||||
} else {
|
||||
bindPhoneNumber = 'NULL';
|
||||
}
|
||||
bindInsert.push(contact.remoteJid);
|
||||
const bindIdentifier = `$${bindInsert.length}`;
|
||||
|
||||
|
||||
@@ -826,7 +826,7 @@ export class ChannelStartupService {
|
||||
const msg = message.message;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import fs from 'fs';
|
||||
import i18next from 'i18next';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.resolve(process.cwd(), 'src', 'utils');
|
||||
|
||||
const languages = ['en', 'pt-BR', 'es'];
|
||||
const translationsPath = path.join(__dirname, 'translations');
|
||||
const configService: ConfigService = new ConfigService();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { prismaRepository } from '@api/server.module';
|
||||
import { configService, Database } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const logger = new Logger('OnWhatsappCache');
|
||||
|
||||
function getAvailableNumbers(remoteJid: string) {
|
||||
const numbersAvailable: string[] = [];
|
||||
|
||||
@@ -11,6 +14,11 @@ function getAvailableNumbers(remoteJid: string) {
|
||||
|
||||
const [number, domain] = remoteJid.split('@');
|
||||
|
||||
// TODO: Se já for @lid, retornar apenas ele mesmo SEM adicionar @domain novamente
|
||||
if (domain === 'lid' || domain === 'g.us') {
|
||||
return [remoteJid]; // Retorna direto para @lid e @g.us
|
||||
}
|
||||
|
||||
// Brazilian numbers
|
||||
if (remoteJid.startsWith('55')) {
|
||||
const numberWithDigit =
|
||||
@@ -47,35 +55,87 @@ function getAvailableNumbers(remoteJid: string) {
|
||||
numbersAvailable.push(remoteJid);
|
||||
}
|
||||
|
||||
// TODO: Adiciona @domain apenas para números que não são @lid
|
||||
return numbersAvailable.map((number) => `${number}@${domain}`);
|
||||
}
|
||||
|
||||
interface ISaveOnWhatsappCacheParams {
|
||||
remoteJid: string;
|
||||
lid?: string;
|
||||
remoteJidAlt?: string;
|
||||
lid?: 'lid' | undefined;
|
||||
}
|
||||
|
||||
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
||||
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
||||
const upsertsQuery = data.map((item) => {
|
||||
for (const item of data) {
|
||||
const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
|
||||
const numbersAvailable = getAvailableNumbers(remoteJid);
|
||||
|
||||
return prismaRepository.isOnWhatsapp.upsert({
|
||||
create: {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: numbersAvailable.join(','),
|
||||
lid: item.lid,
|
||||
// TODO: Buscar registro existente PRIMEIRO para preservar dados
|
||||
const allJids = [remoteJid];
|
||||
|
||||
const altJid =
|
||||
item.remoteJidAlt && item.remoteJidAlt.includes('@lid')
|
||||
? item.remoteJidAlt.startsWith('+')
|
||||
? item.remoteJidAlt.slice(1)
|
||||
: item.remoteJidAlt
|
||||
: null;
|
||||
|
||||
if (altJid) {
|
||||
allJids.push(altJid);
|
||||
}
|
||||
|
||||
const expandedJids = allJids.flatMap((jid) => getAvailableNumbers(jid));
|
||||
|
||||
const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({
|
||||
where: {
|
||||
OR: expandedJids.map((jid) => ({ jidOptions: { contains: jid } })),
|
||||
},
|
||||
update: {
|
||||
jidOptions: numbersAvailable.join(','),
|
||||
lid: item.lid,
|
||||
},
|
||||
where: { remoteJid: remoteJid },
|
||||
});
|
||||
});
|
||||
|
||||
await prismaRepository.$transaction(upsertsQuery);
|
||||
logger.verbose(`Register exists: ${existingRecord ? existingRecord.remoteJid : 'não not found'}`);
|
||||
|
||||
const finalJidOptions = [...expandedJids];
|
||||
|
||||
if (existingRecord?.jidOptions) {
|
||||
const existingJids = existingRecord.jidOptions.split(',');
|
||||
// TODO: Adicionar JIDs existentes que não estão na lista atual
|
||||
existingJids.forEach((jid) => {
|
||||
if (!finalJidOptions.includes(jid)) {
|
||||
finalJidOptions.push(jid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Se tiver remoteJidAlt com @lid novo, adicionar
|
||||
if (altJid && !finalJidOptions.includes(altJid)) {
|
||||
finalJidOptions.push(altJid);
|
||||
}
|
||||
|
||||
const uniqueNumbers = Array.from(new Set(finalJidOptions));
|
||||
|
||||
logger.verbose(
|
||||
`Saving: remoteJid=${remoteJid}, jidOptions=${uniqueNumbers.join(',')}, lid=${item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null}`,
|
||||
);
|
||||
|
||||
if (existingRecord) {
|
||||
await prismaRepository.isOnWhatsapp.update({
|
||||
where: { id: existingRecord.id },
|
||||
data: {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: uniqueNumbers.join(','),
|
||||
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prismaRepository.isOnWhatsapp.create({
|
||||
data: {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: uniqueNumbers.join(','),
|
||||
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user