mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 01:49:37 -06:00
Merge pull request #2103 from KokeroO/develop
feat(baileys,chatwoot,on-whatsapp-cache): implementações e correções na baileys e chatwoot
This commit is contained in:
commit
cd71ff503d
42
package-lock.json
generated
42
package-lock.json
generated
@ -21,7 +21,7 @@
|
|||||||
"amqplib": "^0.10.5",
|
"amqplib": "^0.10.5",
|
||||||
"audio-decode": "^2.2.3",
|
"audio-decode": "^2.2.3",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"baileys": "^7.0.0-rc.5",
|
"baileys": "github:WhiskeySockets/Baileys#master",
|
||||||
"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",
|
||||||
@ -70,8 +70,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"@swc/core": "^1.13.5",
|
|
||||||
"@swc/helpers": "^0.5.17",
|
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.18",
|
"@types/express": "^4.17.18",
|
||||||
@ -4397,8 +4395,9 @@
|
|||||||
"version": "1.13.5",
|
"version": "1.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
|
||||||
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
|
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3",
|
"@swc/counter": "^0.1.3",
|
||||||
"@swc/types": "^0.1.24"
|
"@swc/types": "^0.1.24"
|
||||||
@ -4438,11 +4437,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4454,11 +4453,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4470,11 +4469,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4486,11 +4485,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4502,11 +4501,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4518,11 +4517,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4534,11 +4533,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4550,11 +4549,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4566,11 +4565,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4582,11 +4581,11 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -4595,13 +4594,15 @@
|
|||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
"devOptional": true
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.17",
|
"version": "0.5.17",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||||
"dev": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@ -4610,7 +4611,8 @@
|
|||||||
"version": "0.1.25",
|
"version": "0.1.25",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||||
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
|
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
|
||||||
"devOptional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
}
|
}
|
||||||
@ -5647,14 +5649,14 @@
|
|||||||
},
|
},
|
||||||
"node_modules/baileys": {
|
"node_modules/baileys": {
|
||||||
"version": "7.0.0-rc.5",
|
"version": "7.0.0-rc.5",
|
||||||
"resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.5.tgz",
|
"resolved": "git+ssh://git@github.com/WhiskeySockets/Baileys.git#928fa6ac6669f1621da2ba033192f3661d9c05d0",
|
||||||
"integrity": "sha512-y95gW7UtKbD4dQb46G75rnr0U0LtnBItA002ARggDiCgm92Z8wnM+wxqC8OI/sDFanz3TgzqE4t7MPwNusUqUQ==",
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/node-cache": "^1.4.0",
|
"@cacheable/node-cache": "^1.4.0",
|
||||||
"@hapi/boom": "^9.1.3",
|
"@hapi/boom": "^9.1.3",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git",
|
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
"music-metadata": "^11.7.0",
|
"music-metadata": "^11.7.0",
|
||||||
"p-queue": "^9.0.0",
|
"p-queue": "^9.0.0",
|
||||||
|
|||||||
@ -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.5",
|
"baileys": "github:WhiskeySockets/Baileys#master",
|
||||||
"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 @@
|
|||||||
import { Logger } from '@config/logger.config';
|
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';
|
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
|
||||||
|
|
||||||
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
||||||
@ -12,7 +12,7 @@ export class BaileysMessageProcessor {
|
|||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
|
|
||||||
protected messageSubject = new Subject<{
|
protected messageSubject = new Subject<{
|
||||||
messages: proto.IWebMessageInfo[];
|
messages: WAMessage[];
|
||||||
type: MessageUpsertType;
|
type: MessageUpsertType;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
settings: any;
|
settings: any;
|
||||||
|
|||||||
@ -133,7 +133,6 @@ import { Label } from 'baileys/lib/Types/Label';
|
|||||||
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { isArray, isBase64, isURL } from 'class-validator';
|
import { isArray, isBase64, isURL } from 'class-validator';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
@ -876,6 +875,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
||||||
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
|
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
|
||||||
for await (const contact of contacts) {
|
for await (const contact of contacts) {
|
||||||
|
this.logger.debug(`Updating contact: ${JSON.stringify(contact, null, 2)}`);
|
||||||
contactsRaw.push({
|
contactsRaw.push({
|
||||||
remoteJid: contact.id,
|
remoteJid: contact.id,
|
||||||
pushName: contact?.name ?? contact?.verifiedName,
|
pushName: contact?.name ?? contact?.verifiedName,
|
||||||
@ -895,10 +895,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
await this.prismaRepository.$transaction(updateTransactions);
|
await this.prismaRepository.$transaction(updateTransactions);
|
||||||
|
|
||||||
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
//const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
||||||
if (usersContacts) {
|
|
||||||
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1136,7 +1133,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
const messageKey = `${this.instance.id}_${received.key.id}`;
|
const messageKey = `${this.instance.id}_${received.key.id}`;
|
||||||
const cached = await this.baileysCache.get(messageKey);
|
const cached = await this.baileysCache.get(messageKey);
|
||||||
|
|
||||||
if (cached && !editedMessage) {
|
if (cached && !editedMessage && !requestId) {
|
||||||
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1349,7 +1346,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.verbose(messageRaw);
|
||||||
|
|
||||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
@ -1366,7 +1363,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
|
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,
|
remoteJid: received.key.remoteJid,
|
||||||
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
||||||
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
||||||
@ -1377,6 +1379,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
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) {
|
if (contact) {
|
||||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
||||||
|
|
||||||
@ -1406,10 +1419,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
update: contactRaw,
|
update: contactRaw,
|
||||||
create: contactRaw,
|
create: contactRaw,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contactRaw.remoteJid.includes('@s.whatsapp')) {
|
|
||||||
await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
@ -1417,7 +1426,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
},
|
},
|
||||||
|
|
||||||
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
|
'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}
|
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
||||||
|
|
||||||
@ -1798,7 +1807,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (events['group-participants.update']) {
|
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);
|
this.groupHandler['group-participants.update'](payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1966,6 +1975,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
quoted: any,
|
quoted: any,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
ephemeralExpiration?: number,
|
ephemeralExpiration?: number,
|
||||||
|
contextInfo?: any,
|
||||||
// participants?: GroupParticipant[],
|
// participants?: GroupParticipant[],
|
||||||
) {
|
) {
|
||||||
sender = sender.toLowerCase();
|
sender = sender.toLowerCase();
|
||||||
@ -1982,8 +1992,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
|
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;
|
if (messageId) option.messageId = messageId;
|
||||||
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
|
|
||||||
|
|
||||||
if (message['viewOnceMessage']) {
|
if (message['viewOnceMessage']) {
|
||||||
const m = generateWAMessageFromContent(sender, message, {
|
const m = generateWAMessageFromContent(sender, message, {
|
||||||
@ -2020,10 +2030,19 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contextInfo) {
|
||||||
|
message['contextInfo'] = contextInfo;
|
||||||
|
}
|
||||||
|
|
||||||
if (message['conversation']) {
|
if (message['conversation']) {
|
||||||
return await this.client.sendMessage(
|
return await this.client.sendMessage(
|
||||||
sender,
|
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,
|
option as unknown as MiscMessageGenerationOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2031,7 +2050,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
||||||
return await this.client.sendMessage(
|
return await this.client.sendMessage(
|
||||||
sender,
|
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,
|
option as unknown as MiscMessageGenerationOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2162,7 +2185,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (options?.quoted) {
|
if (options?.quoted) {
|
||||||
const m = 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) {
|
if (msg) {
|
||||||
quoted = msg;
|
quoted = msg;
|
||||||
@ -2172,6 +2195,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
let messageSent: WAMessage;
|
let messageSent: WAMessage;
|
||||||
|
|
||||||
let mentions: string[];
|
let mentions: string[];
|
||||||
|
let contextInfo: any;
|
||||||
|
|
||||||
if (isJidGroup(sender)) {
|
if (isJidGroup(sender)) {
|
||||||
let group;
|
let group;
|
||||||
try {
|
try {
|
||||||
@ -2210,7 +2235,27 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
// group?.participants,
|
// group?.participants,
|
||||||
);
|
);
|
||||||
} else {
|
} 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)) {
|
if (Long.isLong(messageSent?.messageTimestamp)) {
|
||||||
@ -2330,7 +2375,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.verbose(JSON.stringify(messageSent, null, 2));
|
||||||
|
|
||||||
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
||||||
|
|
||||||
@ -3337,120 +3382,128 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
|
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Separate @lid numbers from normal numbers
|
// Unified cache verification for all numbers (normal and LID)
|
||||||
const lidUsers = jids.users.filter(({ jid }) => jid.includes('@lid'));
|
const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', ''));
|
||||||
const normalUsers = jids.users.filter(({ jid }) => !jid.includes('@lid'));
|
|
||||||
|
|
||||||
// For normal numbers, use traditional Baileys verification
|
// Get all numbers from cache
|
||||||
let normalVerifiedUsers: OnWhatsAppDto[] = [];
|
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
||||||
if (normalUsers.length > 0) {
|
|
||||||
console.log('normalUsers', normalUsers);
|
|
||||||
const numbersToVerify = normalUsers.map(({ jid }) => jid.replace('+', ''));
|
|
||||||
console.log('numbersToVerify', numbersToVerify);
|
|
||||||
|
|
||||||
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
// Separate numbers that are and are not in cache
|
||||||
console.log('cachedNumbers', cachedNumbers);
|
const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions));
|
||||||
|
const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid));
|
||||||
|
|
||||||
const filteredNumbers = numbersToVerify.filter(
|
// Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache
|
||||||
(jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)),
|
let verify: { jid: string; exists: boolean }[] = [];
|
||||||
);
|
const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid'));
|
||||||
console.log('filteredNumbers', filteredNumbers);
|
|
||||||
|
|
||||||
const verify = await this.client.onWhatsApp(...filteredNumbers);
|
if (normalNumbersNotInCache.length > 0) {
|
||||||
console.log('verify', verify);
|
this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`);
|
||||||
normalVerifiedUsers = await Promise.all(
|
verify = await this.client.onWhatsApp(...normalNumbersNotInCache);
|
||||||
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;
|
|
||||||
|
|
||||||
return new OnWhatsAppDto(
|
|
||||||
numberJid,
|
|
||||||
!!numberVerified?.exists,
|
|
||||||
user.number,
|
|
||||||
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For @lid numbers, always consider them as valid
|
const verifiedUsers = await Promise.all(
|
||||||
const lidVerifiedUsers: OnWhatsAppDto[] = lidUsers.map((user) => {
|
jids.users.map(async (user) => {
|
||||||
return new OnWhatsAppDto(
|
// Try to get from cache first (works for all: normal and LID)
|
||||||
user.jid,
|
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||||
true,
|
|
||||||
user.number,
|
if (cached) {
|
||||||
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
|
this.logger.verbose(`Number ${user.number} found in cache`);
|
||||||
user.jid.split('@')[1],
|
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
|
// Combine results
|
||||||
onWhatsapp.push(...normalVerifiedUsers, ...lidVerifiedUsers);
|
onWhatsapp.push(...verifiedUsers);
|
||||||
|
|
||||||
// Save to cache only valid numbers
|
// TODO: Salvar no cache apenas números que NÃO estavam no cache
|
||||||
await saveOnWhatsappCache(
|
const numbersToCache = onWhatsapp.filter((user) => {
|
||||||
onWhatsapp
|
if (!user.exists) return false;
|
||||||
.filter((user) => user.exists)
|
// Verifica se estava no cache usando jidOptions
|
||||||
.map((user) => ({
|
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,
|
remoteJid: user.jid,
|
||||||
jidOptions: user.jid.replace('+', ''),
|
lid: user.lid === 'lid' ? 'lid' : undefined,
|
||||||
lid: user.lid,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return onWhatsapp;
|
return onWhatsapp;
|
||||||
}
|
}
|
||||||
@ -3633,10 +3686,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
|
||||||
Object.keys(msg.message).length === 1 &&
|
|
||||||
Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo')
|
|
||||||
) {
|
|
||||||
throw 'The message is messageContextInfo';
|
throw 'The message is messageContextInfo';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,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, WAMessageKey } from 'baileys';
|
import { WAMessageContent, 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';
|
||||||
@ -32,8 +32,6 @@ 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;
|
||||||
@ -45,22 +43,6 @@ 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
|
// Lock polling delay
|
||||||
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||||
|
|
||||||
@ -588,8 +570,10 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createConversation(instance: InstanceDto, body: any) {
|
public async createConversation(instance: InstanceDto, body: any) {
|
||||||
const isLid = body.key.addressingMode === 'lid' && body.key.remoteJidAlt;
|
const isLid = body.key.addressingMode === 'lid';
|
||||||
const remoteJid = isLid ? body.key.remoteJidAlt : body.key.remoteJid;
|
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 cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||||
const maxWaitTime = 5000; // 5 seconds
|
const maxWaitTime = 5000; // 5 seconds
|
||||||
@ -598,19 +582,19 @@ export class ChatwootService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Processa atualização de contatos já criados @lid
|
// Processa atualização de contatos já criados @lid
|
||||||
if (isLid && body.key.remoteJidAlt !== body.key.remoteJid) {
|
if (phoneNumber && remoteJid && !isGroup) {
|
||||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||||
if (contact && contact.identifier !== body.key.remoteJidAlt) {
|
if (contact && contact.identifier !== remoteJid) {
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.remoteJidAlt: ${body.key.remoteJidAlt}`,
|
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
|
||||||
);
|
);
|
||||||
const updateContact = await this.updateContact(instance, contact.id, {
|
const updateContact = await this.updateContact(instance, contact.id, {
|
||||||
identifier: body.key.remoteJidAlt,
|
identifier: remoteJid,
|
||||||
phone_number: `+${body.key.remoteJidAlt.split('@')[0]}`,
|
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateContact === null) {
|
if (updateContact === null) {
|
||||||
const baseContact = await this.findContact(instance, body.key.remoteJidAlt.split('@')[0]);
|
const baseContact = await this.findContact(instance, phoneNumber.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(
|
||||||
@ -626,7 +610,7 @@ export class ChatwootService {
|
|||||||
// If it already exists in the cache, return conversationId
|
// If it already exists in the cache, return conversationId
|
||||||
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: ${phoneNumber}, conversation ID: ${conversationId}`);
|
||||||
let conversationExists: conversation | boolean;
|
let conversationExists: conversation | boolean;
|
||||||
try {
|
try {
|
||||||
conversationExists = await client.conversations.get({
|
conversationExists = await client.conversations.get({
|
||||||
@ -677,8 +661,7 @@ export class ChatwootService {
|
|||||||
return (await this.cache.get(cacheKey)) as number;
|
return (await this.cache.get(cacheKey)) as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGroup = remoteJid.includes('@g.us');
|
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].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;
|
||||||
@ -688,14 +671,15 @@ export class ChatwootService {
|
|||||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||||
|
|
||||||
|
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
|
||||||
nameContact = `${group.subject} (GROUP)`;
|
nameContact = `${group.subject} (GROUP)`;
|
||||||
|
|
||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
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)}`);
|
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)}`);
|
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||||
|
|
||||||
if (findParticipant) {
|
if (findParticipant) {
|
||||||
@ -708,12 +692,12 @@ export class ChatwootService {
|
|||||||
} else {
|
} else {
|
||||||
await this.createContact(
|
await this.createContact(
|
||||||
instance,
|
instance,
|
||||||
body.key.participant.split('@')[0],
|
participantJid.split('@')[0],
|
||||||
filterInbox.id,
|
filterInbox.id,
|
||||||
false,
|
false,
|
||||||
body.pushName,
|
body.pushName,
|
||||||
picture_url.profilePictureUrl || null,
|
picture_url.profilePictureUrl || null,
|
||||||
body.key.participant,
|
participantJid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -721,6 +705,7 @@ export class ChatwootService {
|
|||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
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(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||||
|
|
||||||
|
this.logger.verbose(`Searching contact for: ${chatId}`);
|
||||||
let contact = await this.findContact(instance, chatId);
|
let contact = await this.findContact(instance, chatId);
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
@ -1158,140 +1143,20 @@ 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 {
|
||||||
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
|
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||||
// URLs presigned podem expirar, então convertemos para base64
|
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
||||||
let mediaBuffer: Buffer;
|
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
||||||
let mimeType: string;
|
|
||||||
let fileName: string;
|
|
||||||
|
|
||||||
try {
|
if (!mimeType) {
|
||||||
this.logger.verbose(`Downloading media from: ${media}`);
|
const parts = media.split('/');
|
||||||
|
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]) {
|
||||||
@ -1309,13 +1174,11 @@ 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: base64Audio,
|
audio: media,
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1326,12 +1189,8 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
|
|
||||||
const base64Media = mediaBuffer.toString('base64');
|
|
||||||
|
|
||||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||||
const parsedExt = path.parse(fileName)?.ext;
|
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||||
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
|
|
||||||
type = 'document';
|
type = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,7 +1198,7 @@ export class ChatwootService {
|
|||||||
number: number,
|
number: number,
|
||||||
mediatype: type as any,
|
mediatype: type as any,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
media: base64Media, // Base64 puro, sem prefixo
|
media: media,
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@ -1395,87 +1254,9 @@ 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 {
|
||||||
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
// 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);
|
||||||
|
|
||||||
@ -1494,39 +1275,6 @@ 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 ||
|
||||||
@ -1548,6 +1296,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
|
instance.instanceId = waInstance.instanceId;
|
||||||
|
|
||||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||||
const message = await this.prismaRepository.message.findFirst({
|
const message = await this.prismaRepository.message.findFirst({
|
||||||
@ -1670,63 +1419,44 @@ 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) {
|
||||||
// Processa anexos de forma assíncrona para não bloquear o webhook
|
for (const attachment of message.attachments) {
|
||||||
const processAttachments = async () => {
|
if (!messageReceived) {
|
||||||
for (const attachment of message.attachments) {
|
formatText = null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Executa em background sem bloquear
|
const options: Options = {
|
||||||
processAttachments().catch((error) => {
|
quoted: await this.getQuotedMessage(body, instance),
|
||||||
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
text: formatText,
|
text: formatText,
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
quoted: await this.getQuotedMessage(body, instance),
|
quoted: await this.getQuotedMessage(body, instance),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1744,7 +1474,9 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
messageSent, // Já tem instanceId
|
{
|
||||||
|
...messageSent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
inboxId: body.inbox?.id,
|
inboxId: body.inbox?.id,
|
||||||
@ -1811,7 +1543,7 @@ export class ChatwootService {
|
|||||||
const data: SendTextDto = {
|
const data: SendTextDto = {
|
||||||
number: chatId,
|
number: chatId,
|
||||||
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
@ -1835,55 +1567,6 @@ export class ChatwootService {
|
|||||||
const key = message.key as WAMessageKey;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1896,7 +1579,7 @@ 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" = ${instanceId}
|
WHERE "instanceId" = ${instance.instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1952,11 +1635,12 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const key = message?.key as WAMessageKey;
|
const key = message?.key as WAMessageKey;
|
||||||
|
const messageContent = message?.message as WAMessageContent;
|
||||||
|
|
||||||
if (message && key?.id) {
|
if (messageContent && key?.id) {
|
||||||
return {
|
return {
|
||||||
key: message.key as proto.IMessageKey,
|
key: key,
|
||||||
message: message.message as proto.IMessage,
|
message: messageContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2346,7 +2030,10 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (body.key.remoteJid.includes('@g.us')) {
|
if (body.key.remoteJid.includes('@g.us')) {
|
||||||
const participantName = body.pushName;
|
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})$/);
|
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||||
|
|
||||||
let formattedPhoneNumber: string;
|
let formattedPhoneNumber: string;
|
||||||
@ -2360,9 +2047,11 @@ export class ChatwootService {
|
|||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (!body.key.fromMe) {
|
if (!body.key.fromMe) {
|
||||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
content = bodyMessage
|
||||||
|
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
|
||||||
|
: `**${formattedPhoneNumber} - ${participantName}:**`;
|
||||||
} else {
|
} else {
|
||||||
content = `${bodyMessage}`;
|
content = bodyMessage || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = await this.sendData(
|
const send = await this.sendData(
|
||||||
@ -2487,7 +2176,10 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (body.key.remoteJid.includes('@g.us')) {
|
if (body.key.remoteJid.includes('@g.us')) {
|
||||||
const participantName = body.pushName;
|
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})$/);
|
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||||
|
|
||||||
let formattedPhoneNumber: string;
|
let formattedPhoneNumber: string;
|
||||||
@ -2688,7 +2380,7 @@ export class ChatwootService {
|
|||||||
chatwootImport.clearAll(instance);
|
chatwootImport.clearAll(instance);
|
||||||
}
|
}
|
||||||
// Se não foi via QR code, verifica o throttling.
|
// Se não foi via QR code, verifica o throttling.
|
||||||
else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) {
|
else if (timeSinceLastNotification >= 30000) {
|
||||||
const msgConnection = i18next.t('cw.inbox.connected');
|
const msgConnection = i18next.t('cw.inbox.connected');
|
||||||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||||||
waInstance.lastConnectionNotification = now;
|
waInstance.lastConnectionNotification = now;
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { prismaRepository } from '@api/server.module';
|
import { prismaRepository } from '@api/server.module';
|
||||||
import { configService, Database } from '@config/env.config';
|
import { configService, Database } from '@config/env.config';
|
||||||
|
import { Logger } from '@config/logger.config';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const logger = new Logger('OnWhatsappCache');
|
||||||
|
|
||||||
function getAvailableNumbers(remoteJid: string) {
|
function getAvailableNumbers(remoteJid: string) {
|
||||||
const numbersAvailable: string[] = [];
|
const numbersAvailable: string[] = [];
|
||||||
|
|
||||||
@ -11,6 +14,11 @@ function getAvailableNumbers(remoteJid: string) {
|
|||||||
|
|
||||||
const [number, domain] = remoteJid.split('@');
|
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
|
// Brazilian numbers
|
||||||
if (remoteJid.startsWith('55')) {
|
if (remoteJid.startsWith('55')) {
|
||||||
const numberWithDigit =
|
const numberWithDigit =
|
||||||
@ -47,35 +55,87 @@ function getAvailableNumbers(remoteJid: string) {
|
|||||||
numbersAvailable.push(remoteJid);
|
numbersAvailable.push(remoteJid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Adiciona @domain apenas para números que não são @lid
|
||||||
return numbersAvailable.map((number) => `${number}@${domain}`);
|
return numbersAvailable.map((number) => `${number}@${domain}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISaveOnWhatsappCacheParams {
|
interface ISaveOnWhatsappCacheParams {
|
||||||
remoteJid: string;
|
remoteJid: string;
|
||||||
lid?: string;
|
remoteJidAlt?: string;
|
||||||
|
lid?: 'lid' | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
||||||
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
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 remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
|
||||||
const numbersAvailable = getAvailableNumbers(remoteJid);
|
|
||||||
|
|
||||||
return prismaRepository.isOnWhatsapp.upsert({
|
// TODO: Buscar registro existente PRIMEIRO para preservar dados
|
||||||
create: {
|
const allJids = [remoteJid];
|
||||||
remoteJid: remoteJid,
|
|
||||||
jidOptions: numbersAvailable.join(','),
|
const altJid =
|
||||||
lid: item.lid,
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user