mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 01:49:37 -06:00
Merge branch 'develop' into feature/quote-message-n8n
This commit is contained in:
commit
178386594c
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.6]
|
||||
- Evolution API version: [e.g. 2.3.7]
|
||||
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
|
||||
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
||||
validations:
|
||||
|
||||
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,3 +1,71 @@
|
||||
# 2.3.7 (develop)
|
||||
|
||||
### Features
|
||||
|
||||
* **WhatsApp Business Meta Templates**: Add update and delete endpoints for Meta templates
|
||||
- New endpoints to edit and delete WhatsApp Business templates
|
||||
- Added DTOs and validation schemas for template management
|
||||
- Enhanced template lifecycle management capabilities
|
||||
|
||||
### Fixed
|
||||
|
||||
* **Baileys Message Processor**: Fix incoming message events not working after reconnection
|
||||
- Added cleanup logic in mount() to prevent memory leaks from multiple subscriptions
|
||||
- Recreate messageSubject if it was completed during logout
|
||||
- Remount messageProcessor in connectToWhatsapp() to ensure subscription is active
|
||||
- Fixed issue where onDestroy() calls complete() on RxJS Subject, making it permanently closed
|
||||
- Ensures old subscriptions are properly cleaned up before creating new ones
|
||||
|
||||
* **Baileys Authentication**: Resolve "waiting for message" state after reconnection
|
||||
- Fixed Redis keys not being properly removed during instance logout
|
||||
- Prevented loading of old/invalid cryptographic keys on reconnection
|
||||
- Fixed blocking state where instances authenticate but cannot send messages
|
||||
- Ensures new credentials (creds) are properly used after reconnection
|
||||
|
||||
* **OnWhatsapp Cache**: Prevent unique constraint errors and optimize database writes
|
||||
- Fixed `Unique constraint failed on the fields: (remoteJid)` error when sending to groups
|
||||
- Refactored query to use OR condition finding by jidOptions or remoteJid
|
||||
- Added deep comparison to skip unnecessary database updates
|
||||
- Replaced sequential processing with Promise.allSettled for parallel execution
|
||||
- Sorted JIDs alphabetically in jidOptions for accurate change detection
|
||||
- Added normalizeJid helper function for cleaner code
|
||||
|
||||
* **Proxy Integration**: Fix "Media upload failed on all hosts" error when using proxy
|
||||
- Created makeProxyAgentUndici() for Undici-compatible proxy agents
|
||||
- Fixed compatibility with Node.js 18+ native fetch() implementation
|
||||
- Replaced traditional HttpsProxyAgent/SocksProxyAgent with Undici ProxyAgent
|
||||
- Maintained legacy makeProxyAgent() for Axios compatibility
|
||||
- Fixed protocol handling in makeProxyAgent to prevent undefined errors
|
||||
|
||||
* **WhatsApp Business API**: Fix base64, filename and caption handling
|
||||
- Corrected base64 media conversion in Business API
|
||||
- Fixed filename handling for document messages
|
||||
- Improved caption processing for media messages
|
||||
- Enhanced remoteJid validation and processing
|
||||
|
||||
* **Chat Service**: Fix fetchChats and message panel errors
|
||||
- Fixed cleanMessageData errors in Manager message panel
|
||||
- Improved chat fetching reliability
|
||||
- Enhanced message data sanitization
|
||||
|
||||
* **Contact Filtering**: Apply where filters correctly in findContacts endpoint
|
||||
- Fixed endpoint to process all where clause fields (id, remoteJid, pushName)
|
||||
- Previously only processed remoteJid field, ignoring other filters
|
||||
- Added remoteJid field to contactValidateSchema for proper validation
|
||||
- Maintained multi-tenant isolation with instanceId filtering
|
||||
- Allows filtering contacts by any supported field instead of returning all contacts
|
||||
|
||||
* **Chatwoot and Baileys Integration**: Multiple integration improvements
|
||||
- Enhanced code formatting and consistency
|
||||
- Fixed integration issues between Chatwoot and Baileys services
|
||||
- Improved message handling and delivery
|
||||
|
||||
### Code Quality & Refactoring
|
||||
|
||||
* **Template Management**: Remove unused template edit/delete DTOs after refactoring
|
||||
* **Proxy Utilities**: Improve makeProxyAgent for Undici compatibility
|
||||
* **Code Formatting**: Enhance code formatting and consistency across services
|
||||
|
||||
# 2.3.6 (2025-10-21)
|
||||
|
||||
### Features
|
||||
|
||||
@ -2,7 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
evolution_v2:
|
||||
image: evoapicloud/evolution-api:v2.3.6
|
||||
image: evoapicloud/evolution-api:v2.3.7
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
|
||||
3047
package-lock.json
generated
3047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.3.6",
|
||||
"version": "2.3.7",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
@ -77,7 +77,7 @@
|
||||
"amqplib": "^0.10.5",
|
||||
"audio-decode": "^2.2.3",
|
||||
"axios": "^1.7.9",
|
||||
"baileys": "7.0.0-rc.6",
|
||||
"baileys": "7.0.0-rc.9",
|
||||
"class-validator": "^0.14.1",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
@ -90,12 +90,14 @@
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"form-data": "^4.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"i18next": "^23.7.19",
|
||||
"jimp": "^1.6.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
"libphonenumber-js": "^1.12.25",
|
||||
"link-preview-js": "^3.0.13",
|
||||
"long": "^5.2.3",
|
||||
"mediainfo.js": "^0.3.4",
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
-- 1. Cleanup: Remove duplicate chats, keeping the most recently updated one
|
||||
DELETE FROM "Chat"
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "instanceId", "remoteJid"
|
||||
ORDER BY "updatedAt" DESC
|
||||
) as row_num
|
||||
FROM "Chat"
|
||||
) t
|
||||
WHERE t.row_num > 1
|
||||
);
|
||||
|
||||
-- 2. Create the unique index (Constraint)
|
||||
CREATE UNIQUE INDEX "Chat_instanceId_remoteJid_key" ON "Chat"("instanceId", "remoteJid");
|
||||
@ -132,6 +132,7 @@ model Chat {
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@unique([instanceId, remoteJid])
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
|
||||
@ -92,6 +92,15 @@ export class InstanceController {
|
||||
instanceId: instanceId,
|
||||
});
|
||||
|
||||
const instanceDto: InstanceDto = {
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instance.instanceId,
|
||||
connectionStatus:
|
||||
typeof instance.connectionStatus === 'string'
|
||||
? instance.connectionStatus
|
||||
: instance.connectionStatus?.state || 'unknown',
|
||||
};
|
||||
|
||||
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
|
||||
const testProxy = await this.proxyService.testProxy({
|
||||
host: instanceData.proxyHost,
|
||||
@ -103,8 +112,7 @@ export class InstanceController {
|
||||
if (!testProxy) {
|
||||
throw new BadRequestException('Invalid proxy');
|
||||
}
|
||||
|
||||
await this.proxyService.createProxy(instance, {
|
||||
await this.proxyService.createProxy(instanceDto, {
|
||||
enabled: true,
|
||||
host: instanceData.proxyHost,
|
||||
port: instanceData.proxyPort,
|
||||
@ -125,7 +133,7 @@ export class InstanceController {
|
||||
wavoipToken: instanceData.wavoipToken || '',
|
||||
};
|
||||
|
||||
await this.settingsService.create(instance, settings);
|
||||
await this.settingsService.create(instanceDto, settings);
|
||||
|
||||
let webhookWaBusiness = null,
|
||||
accessTokenWaBusiness = '';
|
||||
@ -155,7 +163,10 @@ export class InstanceController {
|
||||
integration: instanceData.integration,
|
||||
webhookWaBusiness,
|
||||
accessTokenWaBusiness,
|
||||
status: instance.connectionStatus.state,
|
||||
status:
|
||||
typeof instance.connectionStatus === 'string'
|
||||
? instance.connectionStatus
|
||||
: instance.connectionStatus?.state || 'unknown',
|
||||
},
|
||||
hash,
|
||||
webhook: {
|
||||
@ -217,7 +228,7 @@ export class InstanceController {
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
try {
|
||||
this.chatwootService.create(instance, {
|
||||
this.chatwootService.create(instanceDto, {
|
||||
enabled: true,
|
||||
accountId: instanceData.chatwootAccountId,
|
||||
token: instanceData.chatwootToken,
|
||||
@ -246,7 +257,10 @@ export class InstanceController {
|
||||
integration: instanceData.integration,
|
||||
webhookWaBusiness,
|
||||
accessTokenWaBusiness,
|
||||
status: instance.connectionStatus.state,
|
||||
status:
|
||||
typeof instance.connectionStatus === 'string'
|
||||
? instance.connectionStatus
|
||||
: instance.connectionStatus?.state || 'unknown',
|
||||
},
|
||||
hash,
|
||||
webhook: {
|
||||
@ -338,20 +352,38 @@ export class InstanceController {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
|
||||
}
|
||||
|
||||
if (state == 'close') {
|
||||
if (state === 'close') {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
|
||||
} else if (state == 'open') {
|
||||
}
|
||||
this.logger.info(`Restarting instance: ${instanceName}`);
|
||||
|
||||
if (typeof instance.restart === 'function') {
|
||||
await instance.restart();
|
||||
// Wait a bit for the reconnection to be established
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
return {
|
||||
instance: {
|
||||
instanceName: instanceName,
|
||||
status: instance.connectionStatus?.state || 'connecting',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback for Baileys (uses different mechanism)
|
||||
if (state === 'open' || state === 'connecting') {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
|
||||
this.logger.info('restarting instance' + instanceName);
|
||||
|
||||
instance.client?.ws?.close();
|
||||
instance.client?.end(new Error('restart'));
|
||||
return await this.connectToWhatsapp({ instanceName });
|
||||
} else if (state == 'connecting') {
|
||||
instance.client?.ws?.close();
|
||||
instance.client?.end(new Error('restart'));
|
||||
return await this.connectToWhatsapp({ instanceName });
|
||||
}
|
||||
|
||||
return {
|
||||
instance: {
|
||||
instanceName: instanceName,
|
||||
status: state,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return { error: true, message: error.toString() };
|
||||
@ -409,7 +441,7 @@ export class InstanceController {
|
||||
}
|
||||
|
||||
try {
|
||||
this.waMonitor.waInstances[instanceName]?.logoutInstance();
|
||||
await this.waMonitor.waInstances[instanceName]?.logoutInstance();
|
||||
|
||||
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
|
||||
} catch (error) {
|
||||
|
||||
@ -12,4 +12,15 @@ export class TemplateController {
|
||||
public async findTemplate(instance: InstanceDto) {
|
||||
return this.templateService.find(instance);
|
||||
}
|
||||
|
||||
public async editTemplate(
|
||||
instance: InstanceDto,
|
||||
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
|
||||
) {
|
||||
return this.templateService.edit(instance, data);
|
||||
}
|
||||
|
||||
public async deleteTemplate(instance: InstanceDto, data: { name: string; hsmId?: string }) {
|
||||
return this.templateService.delete(instance, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ export class InstanceDto extends IntegrationDto {
|
||||
token?: string;
|
||||
status?: string;
|
||||
ownerJid?: string;
|
||||
connectionStatus?: string;
|
||||
profileName?: string;
|
||||
profilePicUrl?: string;
|
||||
// settings
|
||||
|
||||
@ -6,3 +6,16 @@ export class TemplateDto {
|
||||
components: any;
|
||||
webhookUrl?: string;
|
||||
}
|
||||
|
||||
export class TemplateEditDto {
|
||||
templateId: string;
|
||||
category?: 'AUTHENTICATION' | 'MARKETING' | 'UTILITY';
|
||||
allowCategoryChange?: boolean;
|
||||
ttl?: number;
|
||||
components?: any;
|
||||
}
|
||||
|
||||
export class TemplateDeleteDto {
|
||||
name: string;
|
||||
hsmId?: string;
|
||||
}
|
||||
|
||||
@ -516,7 +516,9 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||
if (this.localWebhook.enabled && this.localWebhook.webhookBase64) {
|
||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||
}
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
||||
@ -554,11 +556,19 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
} else {
|
||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
if (this.localWebhook.enabled && this.localWebhook.webhookBase64) {
|
||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
}
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
||||
let openAiBase64 = messageRaw.message.base64;
|
||||
if (!openAiBase64) {
|
||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||
openAiBase64 = buffer.toString('base64');
|
||||
}
|
||||
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
@ -574,7 +584,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
base64: messageRaw.message.base64,
|
||||
base64: openAiBase64,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
@ -1016,6 +1026,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
[message['mediaType']]: {
|
||||
[message['type']]: message['id'],
|
||||
...(message['mediaType'] !== 'audio' &&
|
||||
message['mediaType'] !== 'video' &&
|
||||
message['fileName'] &&
|
||||
!isImage && { filename: message['fileName'] }),
|
||||
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
||||
@ -1606,9 +1617,14 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
|
||||
const mediaMessage = msg.message[messageType];
|
||||
|
||||
if (!msg.message?.base64) {
|
||||
const buffer = await this.downloadMediaMessage({ type: messageType, ...msg.message });
|
||||
msg.message.base64 = buffer.toString('base64');
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: msg.messageType,
|
||||
fileName: mediaMessage?.fileName,
|
||||
fileName: mediaMessage?.fileName || mediaMessage?.filename,
|
||||
caption: mediaMessage?.caption,
|
||||
size: {
|
||||
fileLength: mediaMessage?.fileLength,
|
||||
|
||||
@ -19,6 +19,22 @@ export class BaileysMessageProcessor {
|
||||
}>();
|
||||
|
||||
mount({ onMessageReceive }: MountProps) {
|
||||
// Se já existe subscription, fazer cleanup primeiro
|
||||
if (this.subscription && !this.subscription.closed) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Se o Subject foi completado, recriar
|
||||
if (this.messageSubject.closed) {
|
||||
this.processorLogs.warn('MessageSubject was closed, recreating...');
|
||||
this.messageSubject = new Subject<{
|
||||
messages: WAMessage[];
|
||||
type: MessageUpsertType;
|
||||
requestId?: string;
|
||||
settings: any;
|
||||
}>();
|
||||
}
|
||||
|
||||
this.subscription = this.messageSubject
|
||||
.pipe(
|
||||
tap(({ messages }) => {
|
||||
|
||||
@ -82,7 +82,7 @@ import { createId as cuid } from '@paralleldrive/cuid2';
|
||||
import { Instance, Message } from '@prisma/client';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
||||
import {makeProxyAgent, makeProxyAgentUndici} from '@utils/makeProxyAgent';
|
||||
import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent';
|
||||
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
@ -99,6 +99,7 @@ import makeWASocket, {
|
||||
Chat,
|
||||
ConnectionState,
|
||||
Contact,
|
||||
decryptPollVote,
|
||||
delay,
|
||||
DisconnectReason,
|
||||
downloadContentFromMessage,
|
||||
@ -113,6 +114,7 @@ import makeWASocket, {
|
||||
isJidGroup,
|
||||
isJidNewsletter,
|
||||
isPnUser,
|
||||
jidNormalizedUser,
|
||||
makeCacheableSignalKeyStore,
|
||||
MessageUpsertType,
|
||||
MessageUserReceiptUpdate,
|
||||
@ -133,6 +135,7 @@ 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 { createHash } from 'crypto';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import FormData from 'form-data';
|
||||
@ -247,6 +250,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
|
||||
private endSession = false;
|
||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||
private eventProcessingQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
// Cache TTL constants (in seconds)
|
||||
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
|
||||
@ -266,6 +270,28 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
this.client?.ws?.close();
|
||||
|
||||
const db = this.configService.get<Database>('DATABASE');
|
||||
const cache = this.configService.get<CacheConf>('CACHE');
|
||||
const provider = this.configService.get<ProviderSession>('PROVIDER');
|
||||
|
||||
if (provider?.ENABLED) {
|
||||
const authState = await this.authStateProvider.authStateProvider(this.instance.id);
|
||||
|
||||
await authState.removeCreds();
|
||||
}
|
||||
|
||||
if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) {
|
||||
const authState = await useMultiFileAuthStateRedisDb(this.instance.id, this.cache);
|
||||
|
||||
await authState.removeCreds();
|
||||
}
|
||||
|
||||
if (db.SAVE_DATA.INSTANCE) {
|
||||
const authState = await useMultiFileAuthStatePrisma(this.instance.id, this.cache);
|
||||
|
||||
await authState.removeCreds();
|
||||
}
|
||||
|
||||
const sessionExists = await this.prismaRepository.session.findFirst({ where: { sessionId: this.instanceId } });
|
||||
if (sessionExists) {
|
||||
await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId } });
|
||||
@ -569,15 +595,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const version = baileysVersion.version;
|
||||
const log = `Baileys version: ${version.join('.')}`;
|
||||
|
||||
// if (session.VERSION) {
|
||||
// version = session.VERSION.split('.');
|
||||
// log = `Baileys version env: ${version}`;
|
||||
// } else {
|
||||
// const baileysVersion = await fetchLatestWaWebVersion({});
|
||||
// version = baileysVersion.version;
|
||||
// log = `Baileys version: ${version}`;
|
||||
// }
|
||||
|
||||
this.logger.info(log);
|
||||
|
||||
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
||||
@ -710,6 +727,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
this.loadWebhook();
|
||||
this.loadProxy();
|
||||
|
||||
// Remontar o messageProcessor para garantir que está funcionando após reconexão
|
||||
this.messageProcessor.mount({
|
||||
onMessageReceive: this.messageHandle['messages.upsert'].bind(this),
|
||||
});
|
||||
|
||||
return await this.createClient(number);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@ -1103,6 +1125,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
);
|
||||
|
||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
||||
|
||||
if (received.key?.id && editedMessage.key?.id) {
|
||||
await this.baileysCache.set(`protocol_${received.key.id}`, editedMessage.key.id, 60 * 60 * 24);
|
||||
}
|
||||
|
||||
const oldMessage = await this.getMessage(editedMessage.key, true);
|
||||
if ((oldMessage as any)?.id) {
|
||||
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
|
||||
@ -1130,22 +1157,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
const messageKey = `${this.instance.id}_${received.key.id}`;
|
||||
const cached = await this.baileysCache.get(messageKey);
|
||||
|
||||
if (cached && !editedMessage && !requestId) {
|
||||
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||
|
||||
if (
|
||||
(type !== 'notify' && type !== 'append') ||
|
||||
editedMessage ||
|
||||
received.message?.pollUpdateMessage ||
|
||||
!received?.message
|
||||
) {
|
||||
if ((type !== 'notify' && type !== 'append') || editedMessage || !received?.message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1185,6 +1197,107 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
const messageRaw = this.prepareMessage(received);
|
||||
|
||||
if (messageRaw.messageType === 'pollUpdateMessage') {
|
||||
const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey;
|
||||
const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo;
|
||||
const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any;
|
||||
|
||||
if (pollMessage) {
|
||||
const pollOptions =
|
||||
(pollMessage.message as any).pollCreationMessage?.options ||
|
||||
(pollMessage.message as any).pollCreationMessageV3?.options ||
|
||||
[];
|
||||
const pollVote = messageRaw.message.pollUpdateMessage.vote;
|
||||
|
||||
const voterJid = received.key.fromMe
|
||||
? this.instance.wuid
|
||||
: received.key.participant || received.key.remoteJid;
|
||||
|
||||
let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret;
|
||||
|
||||
let successfulVoterJid = voterJid;
|
||||
|
||||
if (typeof pollEncKey === 'string') {
|
||||
pollEncKey = Buffer.from(pollEncKey, 'base64');
|
||||
} else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) {
|
||||
pollEncKey = Buffer.from(pollEncKey.data);
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) {
|
||||
pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64');
|
||||
}
|
||||
|
||||
if (pollVote.encPayload && pollEncKey) {
|
||||
const creatorCandidates = [
|
||||
this.instance.wuid,
|
||||
this.client.user?.lid,
|
||||
pollMessage.key.participant,
|
||||
(pollMessage.key as any).participantAlt,
|
||||
pollMessage.key.remoteJid,
|
||||
];
|
||||
|
||||
const key = received.key as any;
|
||||
const voterCandidates = [
|
||||
this.instance.wuid,
|
||||
this.client.user?.lid,
|
||||
key.participant,
|
||||
key.participantAlt,
|
||||
key.remoteJidAlt,
|
||||
key.remoteJid,
|
||||
];
|
||||
|
||||
const uniqueCreators = [
|
||||
...new Set(creatorCandidates.filter(Boolean).map((id) => jidNormalizedUser(id))),
|
||||
];
|
||||
const uniqueVoters = [...new Set(voterCandidates.filter(Boolean).map((id) => jidNormalizedUser(id)))];
|
||||
|
||||
let decryptedVote;
|
||||
|
||||
for (const creator of uniqueCreators) {
|
||||
for (const voter of uniqueVoters) {
|
||||
try {
|
||||
decryptedVote = decryptPollVote(pollVote, {
|
||||
pollCreatorJid: creator,
|
||||
pollMsgId: pollMessage.key.id,
|
||||
pollEncKey,
|
||||
voterJid: voter,
|
||||
} as any);
|
||||
if (decryptedVote) {
|
||||
successfulVoterJid = voter;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Continue trying
|
||||
}
|
||||
}
|
||||
if (decryptedVote) break;
|
||||
}
|
||||
|
||||
if (decryptedVote) {
|
||||
Object.assign(pollVote, decryptedVote);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedOptions = pollVote?.selectedOptions || [];
|
||||
|
||||
const selectedOptionNames = pollOptions
|
||||
.filter((option) => {
|
||||
const hash = createHash('sha256').update(option.optionName).digest();
|
||||
return selectedOptions.some((selected) => Buffer.compare(selected, hash) === 0);
|
||||
})
|
||||
.map((option) => option.optionName);
|
||||
|
||||
messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames;
|
||||
|
||||
const pollUpdates = pollOptions.map((option) => ({
|
||||
name: option.optionName,
|
||||
voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [],
|
||||
}));
|
||||
|
||||
messageRaw.pollUpdates = pollUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
const isMedia =
|
||||
received?.message?.imageMessage ||
|
||||
received?.message?.videoMessage ||
|
||||
@ -1234,7 +1347,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
||||
const msg = await this.prismaRepository.message.create({ data: messageRaw });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { pollUpdates, ...messageData } = messageRaw;
|
||||
const msg = await this.prismaRepository.message.create({ data: messageData });
|
||||
|
||||
const { remoteJid } = received.key;
|
||||
const timestamp = msg.messageTimestamp;
|
||||
@ -1282,6 +1397,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
} else {
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
if (!media) {
|
||||
this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO');
|
||||
return;
|
||||
}
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
const fullName = join(
|
||||
@ -1349,6 +1469,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
this.logger.verbose(messageRaw);
|
||||
|
||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
|
||||
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
|
||||
}
|
||||
console.log(messageRaw);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
|
||||
@ -1435,18 +1559,26 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (update.message !== null && update.status === undefined) continue;
|
||||
|
||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||
|
||||
const cached = await this.baileysCache.get(updateKey);
|
||||
|
||||
if (cached) {
|
||||
this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
||||
const secondsSinceEpoch = Math.floor(Date.now() / 1000);
|
||||
console.log('CACHE:', { cached, updateKey, messageTimestamp: update.messageTimestamp, secondsSinceEpoch });
|
||||
|
||||
if (
|
||||
(update.messageTimestamp && update.messageTimestamp === cached) ||
|
||||
(!update.messageTimestamp && secondsSinceEpoch === cached)
|
||||
) {
|
||||
this.logger.info(`Update Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.baileysCache.set(updateKey, true, 30 * 60);
|
||||
if (update.messageTimestamp) {
|
||||
await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60);
|
||||
} else {
|
||||
await this.baileysCache.set(updateKey, secondsSinceEpoch, 30 * 60);
|
||||
}
|
||||
|
||||
if (status[update.status] === 'READ' && key.fromMe) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
@ -1477,19 +1609,32 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
remoteJid: key?.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.participant,
|
||||
status: status[update.status] ?? 'DELETED',
|
||||
status: status[update.status] ?? 'SERVER_ACK',
|
||||
pollUpdates,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (update.message) {
|
||||
message.message = update.message;
|
||||
}
|
||||
|
||||
let findMessage: any;
|
||||
const configDatabaseData = this.configService.get<Database>('DATABASE').SAVE_DATA;
|
||||
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
||||
// Use raw SQL to avoid JSON path issues
|
||||
const protocolMapKey = `protocol_${key.id}`;
|
||||
const originalMessageId = (await this.baileysCache.get(protocolMapKey)) as string;
|
||||
|
||||
if (originalMessageId) {
|
||||
message.keyId = originalMessageId;
|
||||
}
|
||||
|
||||
const searchId = originalMessageId || key.id;
|
||||
|
||||
const messages = (await this.prismaRepository.$queryRaw`
|
||||
SELECT * FROM "Message"
|
||||
WHERE "instanceId" = ${this.instanceId}
|
||||
AND "key"->>'id' = ${key.id}
|
||||
AND "key"->>'id' = ${searchId}
|
||||
LIMIT 1
|
||||
`) as any[];
|
||||
findMessage = messages[0] || null;
|
||||
@ -1502,7 +1647,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if (update.message === null && update.status === undefined) {
|
||||
this.sendDataWebhook(Events.MESSAGES_DELETE, key);
|
||||
this.sendDataWebhook(Events.MESSAGES_DELETE, { ...key, status: 'DELETED' });
|
||||
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
||||
await this.prismaRepository.messageUpdate.create({ data: message });
|
||||
@ -1550,8 +1695,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
||||
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
||||
await this.prismaRepository.messageUpdate.create({ data: message });
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { message: _msg, ...messageData } = message;
|
||||
await this.prismaRepository.messageUpdate.create({ data: messageData });
|
||||
}
|
||||
|
||||
const existingChat = await this.prismaRepository.chat.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
||||
@ -1602,9 +1750,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
// This enables LID to phoneNumber conversion without breaking existing webhook consumers
|
||||
|
||||
// Helper to normalize participantId as phone number
|
||||
const normalizePhoneNumber = (id: string): string => {
|
||||
const normalizePhoneNumber = (id: string | null | undefined): string => {
|
||||
// Remove @lid, @s.whatsapp.net suffixes and extract just the number part
|
||||
return id.split('@')[0];
|
||||
return String(id || '').split('@')[0];
|
||||
};
|
||||
|
||||
try {
|
||||
@ -1720,135 +1868,141 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
private eventHandler() {
|
||||
this.client.ev.process(async (events) => {
|
||||
if (!this.endSession) {
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
this.eventProcessingQueue = this.eventProcessingQueue.then(async () => {
|
||||
try {
|
||||
if (!this.endSession) {
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
|
||||
if (events.call) {
|
||||
const call = events.call[0];
|
||||
if (events.call) {
|
||||
const call = events.call[0];
|
||||
|
||||
if (settings?.rejectCall && call.status == 'offer') {
|
||||
this.client.rejectCall(call.id, call.from);
|
||||
}
|
||||
if (settings?.rejectCall && call.status == 'offer') {
|
||||
this.client.rejectCall(call.id, call.from);
|
||||
}
|
||||
|
||||
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);
|
||||
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' });
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.CALL, call);
|
||||
}
|
||||
const msg = await this.client.sendMessage(call.from, { text: settings.msgCall });
|
||||
|
||||
this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' });
|
||||
}
|
||||
if (events['connection.update']) {
|
||||
this.connectionUpdate(events['connection.update']);
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.CALL, call);
|
||||
}
|
||||
if (events['creds.update']) {
|
||||
this.instance.authState.saveCreds();
|
||||
}
|
||||
|
||||
if (events['connection.update']) {
|
||||
this.connectionUpdate(events['connection.update']);
|
||||
}
|
||||
if (events['messaging-history.set']) {
|
||||
const payload = events['messaging-history.set'];
|
||||
await this.messageHandle['messaging-history.set'](payload);
|
||||
}
|
||||
|
||||
if (events['creds.update']) {
|
||||
this.instance.authState.saveCreds();
|
||||
}
|
||||
if (events['messages.upsert']) {
|
||||
const payload = events['messages.upsert'];
|
||||
|
||||
if (events['messaging-history.set']) {
|
||||
const payload = events['messaging-history.set'];
|
||||
this.messageHandle['messaging-history.set'](payload);
|
||||
}
|
||||
// this.messageProcessor.processMessage(payload, settings);
|
||||
await this.messageHandle['messages.upsert'](payload, settings);
|
||||
}
|
||||
|
||||
if (events['messages.upsert']) {
|
||||
const payload = events['messages.upsert'];
|
||||
if (events['messages.update']) {
|
||||
const payload = events['messages.update'];
|
||||
await this.messageHandle['messages.update'](payload, settings);
|
||||
}
|
||||
|
||||
this.messageProcessor.processMessage(payload, settings);
|
||||
// this.messageHandle['messages.upsert'](payload, settings);
|
||||
}
|
||||
if (events['message-receipt.update']) {
|
||||
const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[];
|
||||
const remotesJidMap: Record<string, number> = {};
|
||||
|
||||
if (events['messages.update']) {
|
||||
const payload = events['messages.update'];
|
||||
this.messageHandle['messages.update'](payload, settings);
|
||||
}
|
||||
for (const event of payload) {
|
||||
if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') {
|
||||
remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
if (events['message-receipt.update']) {
|
||||
const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[];
|
||||
const remotesJidMap: Record<string, number> = {};
|
||||
await Promise.all(
|
||||
Object.keys(remotesJidMap).map(async (remoteJid) =>
|
||||
this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const event of payload) {
|
||||
if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') {
|
||||
remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp;
|
||||
if (events['presence.update']) {
|
||||
const payload = events['presence.update'];
|
||||
|
||||
if (settings?.groupsIgnore && payload.id.includes('@g.us')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.PRESENCE_UPDATE, payload);
|
||||
}
|
||||
|
||||
if (!settings?.groupsIgnore) {
|
||||
if (events['groups.upsert']) {
|
||||
const payload = events['groups.upsert'];
|
||||
this.groupHandler['groups.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['groups.update']) {
|
||||
const payload = events['groups.update'];
|
||||
this.groupHandler['groups.update'](payload);
|
||||
}
|
||||
|
||||
if (events['group-participants.update']) {
|
||||
const payload = events['group-participants.update'] as any;
|
||||
this.groupHandler['group-participants.update'](payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (events['chats.upsert']) {
|
||||
const payload = events['chats.upsert'];
|
||||
this.chatHandle['chats.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['chats.update']) {
|
||||
const payload = events['chats.update'];
|
||||
this.chatHandle['chats.update'](payload);
|
||||
}
|
||||
|
||||
if (events['chats.delete']) {
|
||||
const payload = events['chats.delete'];
|
||||
this.chatHandle['chats.delete'](payload);
|
||||
}
|
||||
|
||||
if (events['contacts.upsert']) {
|
||||
const payload = events['contacts.upsert'];
|
||||
this.contactHandle['contacts.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['contacts.update']) {
|
||||
const payload = events['contacts.update'];
|
||||
this.contactHandle['contacts.update'](payload);
|
||||
}
|
||||
|
||||
if (events[Events.LABELS_ASSOCIATION]) {
|
||||
const payload = events[Events.LABELS_ASSOCIATION];
|
||||
this.labelHandle[Events.LABELS_ASSOCIATION](payload, database);
|
||||
return;
|
||||
}
|
||||
|
||||
if (events[Events.LABELS_EDIT]) {
|
||||
const payload = events[Events.LABELS_EDIT];
|
||||
this.labelHandle[Events.LABELS_EDIT](payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(remotesJidMap).map(async (remoteJid) =>
|
||||
this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
if (events['presence.update']) {
|
||||
const payload = events['presence.update'];
|
||||
|
||||
if (settings?.groupsIgnore && payload.id.includes('@g.us')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.PRESENCE_UPDATE, payload);
|
||||
}
|
||||
|
||||
if (!settings?.groupsIgnore) {
|
||||
if (events['groups.upsert']) {
|
||||
const payload = events['groups.upsert'];
|
||||
this.groupHandler['groups.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['groups.update']) {
|
||||
const payload = events['groups.update'];
|
||||
this.groupHandler['groups.update'](payload);
|
||||
}
|
||||
|
||||
if (events['group-participants.update']) {
|
||||
const payload = events['group-participants.update'] as any;
|
||||
this.groupHandler['group-participants.update'](payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (events['chats.upsert']) {
|
||||
const payload = events['chats.upsert'];
|
||||
this.chatHandle['chats.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['chats.update']) {
|
||||
const payload = events['chats.update'];
|
||||
this.chatHandle['chats.update'](payload);
|
||||
}
|
||||
|
||||
if (events['chats.delete']) {
|
||||
const payload = events['chats.delete'];
|
||||
this.chatHandle['chats.delete'](payload);
|
||||
}
|
||||
|
||||
if (events['contacts.upsert']) {
|
||||
const payload = events['contacts.upsert'];
|
||||
this.contactHandle['contacts.upsert'](payload);
|
||||
}
|
||||
|
||||
if (events['contacts.update']) {
|
||||
const payload = events['contacts.update'];
|
||||
this.contactHandle['contacts.update'](payload);
|
||||
}
|
||||
|
||||
if (events[Events.LABELS_ASSOCIATION]) {
|
||||
const payload = events[Events.LABELS_ASSOCIATION];
|
||||
this.labelHandle[Events.LABELS_ASSOCIATION](payload, database);
|
||||
return;
|
||||
}
|
||||
|
||||
if (events[Events.LABELS_EDIT]) {
|
||||
const payload = events[Events.LABELS_EDIT];
|
||||
this.labelHandle[Events.LABELS_EDIT](payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -2314,6 +2468,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
} else {
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
if (!media) {
|
||||
this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO');
|
||||
return;
|
||||
}
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
@ -3687,7 +3846,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
|
||||
throw 'The message is messageContextInfo';
|
||||
this.logger.verbose('Message contains only messageContextInfo, skipping media processing');
|
||||
return null;
|
||||
}
|
||||
|
||||
let mediaMessage: any;
|
||||
@ -4870,7 +5030,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
AND: [
|
||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
||||
{
|
||||
OR: [
|
||||
@ -4900,7 +5059,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
AND: [
|
||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
||||
{
|
||||
OR: [
|
||||
|
||||
@ -211,7 +211,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
try {
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
@ -219,7 +219,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
@ -290,7 +290,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
linkPreview,
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { waMonitor } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { CacheEngine } from '@cache/cacheengine';
|
||||
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isURL } from 'class-validator';
|
||||
@ -13,7 +9,6 @@ export class ChatwootController {
|
||||
constructor(
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
||||
@ -84,9 +79,6 @@ export class ChatwootController {
|
||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
|
||||
const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache);
|
||||
|
||||
return chatwootService.receiveWebhook(instance, data);
|
||||
return this.chatwootService.receiveWebhook(instance, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import { WAMessageContent, WAMessageKey } from 'baileys';
|
||||
import dayjs from 'dayjs';
|
||||
import FormData from 'form-data';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||
import Long from 'long';
|
||||
import mimeTypes from 'mime-types';
|
||||
import path from 'path';
|
||||
@ -345,6 +346,16 @@ export class ChatwootService {
|
||||
|
||||
return contact;
|
||||
} catch (error) {
|
||||
if ((error.status === 422 || error.response?.status === 422) && jid) {
|
||||
this.logger.warn(`Contact with identifier ${jid} creation failed (422). Checking if it already exists...`);
|
||||
const existingContact = await this.findContactByIdentifier(instance, jid);
|
||||
if (existingContact) {
|
||||
const contactId = existingContact.id;
|
||||
await this.addLabelToContact(this.provider.nameInbox, contactId);
|
||||
return existingContact;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error('Error creating contact');
|
||||
console.log(error);
|
||||
return null;
|
||||
@ -414,6 +425,55 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
public async findContactByIdentifier(instance: InstanceDto, identifier: string) {
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn('client not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direct search by query (q) - most common way to search by identifier/email/phone
|
||||
const contact = (await (client as any).get('contacts/search', {
|
||||
params: {
|
||||
q: identifier,
|
||||
sort: 'name',
|
||||
},
|
||||
})) as any;
|
||||
|
||||
if (contact && contact.data && contact.data.payload && contact.data.payload.length > 0) {
|
||||
return contact.data.payload[0];
|
||||
}
|
||||
|
||||
// Fallback for older API versions or different response structures
|
||||
if (contact && contact.payload && contact.payload.length > 0) {
|
||||
return contact.payload[0];
|
||||
}
|
||||
|
||||
// Try search by attribute
|
||||
const contactByAttr = (await (client as any).post('contacts/filter', {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'identifier',
|
||||
filter_operator: 'equal_to',
|
||||
values: [identifier],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
})) as any;
|
||||
|
||||
if (contactByAttr && contactByAttr.payload && contactByAttr.payload.length > 0) {
|
||||
return contactByAttr.payload[0];
|
||||
}
|
||||
|
||||
// Check inside data property if using axios interceptors wrapper
|
||||
if (contactByAttr && contactByAttr.data && contactByAttr.data.payload && contactByAttr.data.payload.length > 0) {
|
||||
return contactByAttr.data.payload[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async findContact(instance: InstanceDto, phoneNumber: string) {
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
@ -589,7 +649,7 @@ export class ChatwootService {
|
||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
|
||||
);
|
||||
const updateContact = await this.updateContact(instance, contact.id, {
|
||||
identifier: remoteJid,
|
||||
identifier: phoneNumber,
|
||||
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||
});
|
||||
|
||||
@ -611,13 +671,15 @@ export class ChatwootService {
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
|
||||
let conversationExists: conversation | boolean;
|
||||
let conversationExists: any;
|
||||
try {
|
||||
conversationExists = await client.conversations.get({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversationId,
|
||||
});
|
||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
||||
this.logger.verbose(
|
||||
`Conversation exists: ID: ${conversationExists.id} - Name: ${conversationExists.meta.sender.name} - Identifier: ${conversationExists.meta.sender.identifier}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting conversation: ${error}`);
|
||||
conversationExists = false;
|
||||
@ -669,7 +731,7 @@ export class ChatwootService {
|
||||
if (isGroup) {
|
||||
this.logger.verbose(`Processing group conversation`);
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);
|
||||
|
||||
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
@ -680,9 +742,11 @@ export class ChatwootService {
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
this.logger.verbose(
|
||||
`Found participant: ID:${findParticipant.id} - Name: ${findParticipant.name} - identifier: ${findParticipant.identifier}`,
|
||||
);
|
||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||
await this.updateContact(instance, findParticipant.id, {
|
||||
name: body.pushName,
|
||||
@ -692,7 +756,7 @@ export class ChatwootService {
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
participantJid.split('@')[0],
|
||||
participantJid.split('@')[0].split(':')[0],
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
@ -709,20 +773,13 @@ export class ChatwootService {
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
|
||||
if (contact) {
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
this.logger.verbose(`Found contact: ID:${contact.id} - Name:${contact.name}`);
|
||||
if (!body.key.fromMe) {
|
||||
const waProfilePictureFile =
|
||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
||||
const nameNeedsUpdate =
|
||||
!contact.name ||
|
||||
contact.name === chatId ||
|
||||
(`+${chatId}`.startsWith('+55')
|
||||
? this.getNumbers(`+${chatId}`).some(
|
||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
||||
)
|
||||
: false);
|
||||
const nameNeedsUpdate = !contact.name || contact.name === chatId;
|
||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||
@ -741,7 +798,7 @@ export class ChatwootService {
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
remoteJid,
|
||||
phoneNumber,
|
||||
);
|
||||
}
|
||||
|
||||
@ -757,7 +814,6 @@ export class ChatwootService {
|
||||
accountId: this.provider.accountId,
|
||||
id: contactId,
|
||||
})) as any;
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error(`No conversations found or payload is undefined`);
|
||||
@ -769,7 +825,9 @@ export class ChatwootService {
|
||||
);
|
||||
if (inboxConversation) {
|
||||
if (this.provider.reopenConversation) {
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
||||
this.logger.verbose(
|
||||
`Found conversation in reopenConversation mode: ID: ${inboxConversation.id} - Name: ${inboxConversation.meta.sender.name} - Identifier: ${inboxConversation.meta.sender.identifier}`,
|
||||
);
|
||||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||||
await client.conversations.toggleStatus({
|
||||
accountId: this.provider.accountId,
|
||||
@ -789,7 +847,7 @@ export class ChatwootService {
|
||||
|
||||
if (inboxConversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||
this.cache.set(cacheKey, inboxConversation.id, 8 * 3600);
|
||||
this.cache.set(cacheKey, inboxConversation.id, 1800);
|
||||
return inboxConversation.id;
|
||||
}
|
||||
}
|
||||
@ -803,14 +861,6 @@ export class ChatwootService {
|
||||
data['status'] = 'pending';
|
||||
}
|
||||
|
||||
/*
|
||||
Triple check after lock
|
||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||||
*/
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const conversation = await client.conversations.create({
|
||||
accountId: this.provider.accountId,
|
||||
data,
|
||||
@ -822,7 +872,7 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id, 8 * 3600);
|
||||
this.cache.set(cacheKey, conversation.id, 1800);
|
||||
return conversation.id;
|
||||
} finally {
|
||||
await this.cache.delete(lockKey);
|
||||
@ -1392,10 +1442,7 @@ 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:' &&
|
||||
body?.conversation?.messages[0]?.id === body?.id
|
||||
) {
|
||||
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
|
||||
return { message: 'bot' };
|
||||
}
|
||||
|
||||
@ -1586,7 +1633,11 @@ export class ChatwootService {
|
||||
this.logger.verbose(`Update result: ${result} rows affected`);
|
||||
|
||||
if (this.isImportHistoryAvailable()) {
|
||||
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||||
try {
|
||||
await chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating Chatwoot message source ID: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1666,6 +1717,10 @@ export class ChatwootService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private isInteractiveButtonMessage(messageType: string, message: any) {
|
||||
return messageType === 'interactiveMessage' && message.interactiveMessage?.nativeFlowMessage?.buttons?.length > 0;
|
||||
}
|
||||
|
||||
private getAdsMessage(msg: any) {
|
||||
interface AdsMessage {
|
||||
title: string;
|
||||
@ -1984,8 +2039,9 @@ export class ChatwootService {
|
||||
const adsMessage = this.getAdsMessage(body);
|
||||
|
||||
const reactionMessage = this.getReactionMessage(body.message);
|
||||
const isInteractiveButtonMessage = this.isInteractiveButtonMessage(body.messageType, body.message);
|
||||
|
||||
if (!bodyMessage && !isMedia && !reactionMessage) {
|
||||
if (!bodyMessage && !isMedia && !reactionMessage && !isInteractiveButtonMessage) {
|
||||
this.logger.warn('no body message found');
|
||||
return;
|
||||
}
|
||||
@ -2031,18 +2087,10 @@ export class ChatwootService {
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
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;
|
||||
|
||||
if (phoneMatch) {
|
||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||||
} else {
|
||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||||
}
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
|
||||
? body.key.participantAlt.split('@')[0].split(':')[0]
|
||||
: body.key.participant.split('@')[0].split(':')[0];
|
||||
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
|
||||
|
||||
let content: string;
|
||||
|
||||
@ -2118,6 +2166,50 @@ export class ChatwootService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInteractiveButtonMessage) {
|
||||
const buttons = body.message.interactiveMessage.nativeFlowMessage.buttons;
|
||||
this.logger.info('is Interactive Button Message: ' + JSON.stringify(buttons));
|
||||
|
||||
for (const button of buttons) {
|
||||
const buttonParams = JSON.parse(button.buttonParamsJson);
|
||||
const paymentSettings = buttonParams.payment_settings;
|
||||
|
||||
if (button.name === 'payment_info' && paymentSettings[0].type === 'pix_static_code') {
|
||||
const pixSettings = paymentSettings[0].pix_static_code;
|
||||
const pixKeyType = (() => {
|
||||
switch (pixSettings.key_type) {
|
||||
case 'EVP':
|
||||
return 'Chave Aleatória';
|
||||
case 'EMAIL':
|
||||
return 'E-mail';
|
||||
case 'PHONE':
|
||||
return 'Telefone';
|
||||
default:
|
||||
return pixSettings.key_type;
|
||||
}
|
||||
})();
|
||||
const pixKey = pixSettings.key_type === 'PHONE' ? pixSettings.key.replace('+55', '') : pixSettings.key;
|
||||
const content = `*${pixSettings.merchant_name}*\nChave PIX: ${pixKey} (${pixKeyType})`;
|
||||
|
||||
const send = await this.createMessage(
|
||||
instance,
|
||||
getConversation,
|
||||
content,
|
||||
messageType,
|
||||
false,
|
||||
[],
|
||||
body,
|
||||
'WAID:' + body.key.id,
|
||||
quotedMsg,
|
||||
);
|
||||
if (!send) this.logger.warn('message not sent');
|
||||
} else {
|
||||
this.logger.warn('Interactive Button Message not mapped');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl;
|
||||
if (isAdsMessage) {
|
||||
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
|
||||
@ -2177,18 +2269,10 @@ export class ChatwootService {
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
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;
|
||||
|
||||
if (phoneMatch) {
|
||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||||
} else {
|
||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||||
}
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
|
||||
? body.key.participantAlt.split('@')[0].split(':')[0]
|
||||
: body.key.participant.split('@')[0].split(':')[0];
|
||||
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
|
||||
|
||||
let content: string;
|
||||
|
||||
@ -2270,8 +2354,21 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||
const editedMessageContent =
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
|
||||
const editedMessageContentRaw =
|
||||
body?.editedMessage?.conversation ??
|
||||
body?.editedMessage?.extendedTextMessage?.text ??
|
||||
body?.editedMessage?.imageMessage?.caption ??
|
||||
body?.editedMessage?.videoMessage?.caption ??
|
||||
body?.editedMessage?.documentMessage?.caption ??
|
||||
(typeof body?.text === 'string' ? body.text : undefined);
|
||||
|
||||
const editedMessageContent = (editedMessageContentRaw ?? '').trim();
|
||||
|
||||
if (!editedMessageContent) {
|
||||
this.logger.info('[CW.EDIT] Conteúdo vazio — ignorando (DELETE tratará se for revoke).');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||
|
||||
if (!message) {
|
||||
@ -2338,7 +2435,7 @@ export class ChatwootService {
|
||||
const url =
|
||||
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
|
||||
`/conversations/${conversationId}/update_last_seen`;
|
||||
chatwootRequest(this.getClientCwConfig(), {
|
||||
await chatwootRequest(this.getClientCwConfig(), {
|
||||
method: 'POST',
|
||||
url: url,
|
||||
});
|
||||
@ -2430,7 +2527,13 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
public getNumberFromRemoteJid(remoteJid: string) {
|
||||
public normalizeJidIdentifier(remoteJid: string) {
|
||||
if (!remoteJid) {
|
||||
return '';
|
||||
}
|
||||
if (remoteJid.includes('@lid')) {
|
||||
return remoteJid;
|
||||
}
|
||||
return remoteJid.replace(/:\d+/, '').split('@')[0];
|
||||
}
|
||||
|
||||
|
||||
@ -137,7 +137,7 @@ class ChatwootImport {
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
phone_number = EXCLUDED.phone_number,
|
||||
identifier = EXCLUDED.identifier`;
|
||||
updated_at = NOW()`;
|
||||
|
||||
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
||||
|
||||
|
||||
@ -327,7 +327,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
if (message.type === 'image') {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: session.remoteJid.split('@')[0],
|
||||
number: session.remoteJid,
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: 'image',
|
||||
media: message.content.url,
|
||||
@ -342,7 +342,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
if (message.type === 'video') {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: session.remoteJid.split('@')[0],
|
||||
number: session.remoteJid,
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: 'video',
|
||||
media: message.content.url,
|
||||
@ -357,7 +357,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
if (message.type === 'audio') {
|
||||
await instance.audioWhatsapp(
|
||||
{
|
||||
number: session.remoteJid.split('@')[0],
|
||||
number: session.remoteJid,
|
||||
delay: settings?.delayMessage || 1000,
|
||||
encoding: true,
|
||||
audio: message.content.url,
|
||||
@ -441,7 +441,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
*/
|
||||
private async processListMessage(instance: any, formattedText: string, remoteJid: string) {
|
||||
const listJson = {
|
||||
number: remoteJid.split('@')[0],
|
||||
number: remoteJid,
|
||||
title: '',
|
||||
description: '',
|
||||
buttonText: '',
|
||||
@ -490,7 +490,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
*/
|
||||
private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) {
|
||||
const buttonJson = {
|
||||
number: remoteJid.split('@')[0],
|
||||
number: remoteJid,
|
||||
thumbnailUrl: undefined,
|
||||
title: '',
|
||||
description: '',
|
||||
|
||||
@ -33,7 +33,8 @@ export class WebsocketController extends EventController implements EventControl
|
||||
const { remoteAddress } = req.socket;
|
||||
const websocketConfig = configService.get<Websocket>('WEBSOCKET');
|
||||
const allowedHosts = websocketConfig.ALLOWED_HOSTS || '127.0.0.1,::1,::ffff:127.0.0.1';
|
||||
const isAllowedHost = allowedHosts
|
||||
const allowAllHosts = allowedHosts.trim() === '*';
|
||||
const isAllowedHost = allowAllHosts || allowedHosts
|
||||
.split(',')
|
||||
.map((h) => h.trim())
|
||||
.includes(remoteAddress);
|
||||
|
||||
@ -48,9 +48,14 @@ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
const metricsIPWhitelist = (req: Request, res: Response, next: NextFunction) => {
|
||||
const metricsConfig = configService.get('METRICS');
|
||||
const allowedIPs = metricsConfig.ALLOWED_IPS?.split(',').map((ip) => ip.trim()) || ['127.0.0.1'];
|
||||
const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;
|
||||
const clientIPs = [
|
||||
req.ip,
|
||||
req.connection.remoteAddress,
|
||||
req.socket.remoteAddress,
|
||||
req.headers['x-forwarded-for'],
|
||||
].filter((ip) => ip !== undefined);
|
||||
|
||||
if (!allowedIPs.includes(clientIP)) {
|
||||
if (allowedIPs.filter((ip) => clientIPs.includes(ip)) === 0) {
|
||||
return res.status(403).send('Forbidden: IP not allowed');
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TemplateDto } from '@api/dto/template.dto';
|
||||
import { TemplateDeleteDto, TemplateDto, TemplateEditDto } from '@api/dto/template.dto';
|
||||
import { templateController } from '@api/server.module';
|
||||
import { ConfigService } from '@config/env.config';
|
||||
import { createMetaErrorResponse } from '@utils/errorResponse';
|
||||
import { templateDeleteSchema } from '@validate/templateDelete.schema';
|
||||
import { templateEditSchema } from '@validate/templateEdit.schema';
|
||||
import { instanceSchema, templateSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
@ -35,6 +37,38 @@ export class TemplateRouter extends RouterBroker {
|
||||
res.status(errorResponse.status).json(errorResponse);
|
||||
}
|
||||
})
|
||||
.post(this.routerPath('edit'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<TemplateEditDto>({
|
||||
request: req,
|
||||
schema: templateEditSchema,
|
||||
ClassRef: TemplateEditDto,
|
||||
execute: (instance, data) => templateController.editTemplate(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
} catch (error) {
|
||||
console.error('Template edit error:', error);
|
||||
const errorResponse = createMetaErrorResponse(error, 'template_edit');
|
||||
res.status(errorResponse.status).json(errorResponse);
|
||||
}
|
||||
})
|
||||
.delete(this.routerPath('delete'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<TemplateDeleteDto>({
|
||||
request: req,
|
||||
schema: templateDeleteSchema,
|
||||
ClassRef: TemplateDeleteDto,
|
||||
execute: (instance, data) => templateController.deleteTemplate(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
} catch (error) {
|
||||
console.error('Template delete error:', error);
|
||||
const errorResponse = createMetaErrorResponse(error, 'template_delete');
|
||||
res.status(errorResponse.status).json(errorResponse);
|
||||
}
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
|
||||
@ -82,7 +82,7 @@ const proxyService = new ProxyService(waMonitor);
|
||||
export const proxyController = new ProxyController(proxyService, waMonitor);
|
||||
|
||||
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
|
||||
export const chatwootController = new ChatwootController(chatwootService, configService, prismaRepository);
|
||||
export const chatwootController = new ChatwootController(chatwootService, configService);
|
||||
|
||||
const settingsService = new SettingsService(waMonitor);
|
||||
export const settingsController = new SettingsController(settingsService);
|
||||
|
||||
@ -60,6 +60,7 @@ export class ChannelStartupService {
|
||||
this.instance.number = instance.number;
|
||||
this.instance.token = instance.token;
|
||||
this.instance.businessId = instance.businessId;
|
||||
this.instance.ownerJid = instance.ownerJid;
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
@ -490,20 +491,23 @@ export class ChannelStartupService {
|
||||
}
|
||||
|
||||
public async fetchContacts(query: Query<Contact>) {
|
||||
const remoteJid = query?.where?.remoteJid
|
||||
? query?.where?.remoteJid.includes('@')
|
||||
? query.where?.remoteJid
|
||||
: createJid(query.where?.remoteJid)
|
||||
: null;
|
||||
|
||||
const where = {
|
||||
const where: any = {
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (remoteJid) {
|
||||
if (query?.where?.remoteJid) {
|
||||
const remoteJid = query.where.remoteJid.includes('@') ? query.where.remoteJid : createJid(query.where.remoteJid);
|
||||
where['remoteJid'] = remoteJid;
|
||||
}
|
||||
|
||||
if (query?.where?.id) {
|
||||
where['id'] = query.where.id;
|
||||
}
|
||||
|
||||
if (query?.where?.pushName) {
|
||||
where['pushName'] = query.where.pushName;
|
||||
}
|
||||
|
||||
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
||||
where,
|
||||
};
|
||||
@ -532,14 +536,12 @@ export class ChannelStartupService {
|
||||
|
||||
public cleanMessageData(message: any) {
|
||||
if (!message) return message;
|
||||
|
||||
const cleanedMessage = { ...message };
|
||||
|
||||
const mediaUrl = cleanedMessage.message.mediaUrl;
|
||||
|
||||
delete cleanedMessage.message.base64;
|
||||
|
||||
if (cleanedMessage.message) {
|
||||
const { mediaUrl } = cleanedMessage.message;
|
||||
delete cleanedMessage.message.base64;
|
||||
|
||||
// Limpa imageMessage
|
||||
if (cleanedMessage.message.imageMessage) {
|
||||
cleanedMessage.message.imageMessage = {
|
||||
@ -581,9 +583,9 @@ export class ChannelStartupService {
|
||||
name: cleanedMessage.message.documentWithCaptionMessage.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
|
||||
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
|
||||
}
|
||||
|
||||
return cleanedMessage;
|
||||
}
|
||||
|
||||
@ -38,25 +38,37 @@ export class WAMonitoringService {
|
||||
|
||||
private readonly logger = new Logger('WAMonitoringService');
|
||||
public readonly waInstances: Record<string, any> = {};
|
||||
private readonly delInstanceTimeouts: Record<string, NodeJS.Timeout> = {};
|
||||
|
||||
private readonly providerSession: ProviderSession;
|
||||
|
||||
public delInstanceTime(instance: string) {
|
||||
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
|
||||
if (typeof time === 'number' && time > 0) {
|
||||
setTimeout(
|
||||
// Clear previous timeout if exists
|
||||
if (this.delInstanceTimeouts[instance]) {
|
||||
clearTimeout(this.delInstanceTimeouts[instance]);
|
||||
}
|
||||
|
||||
// Set new timeout and store reference
|
||||
this.delInstanceTimeouts[instance] = setTimeout(
|
||||
async () => {
|
||||
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
|
||||
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
|
||||
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
|
||||
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
|
||||
this.waInstances[instance]?.client?.ws?.close();
|
||||
this.waInstances[instance]?.client?.end(undefined);
|
||||
try {
|
||||
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
|
||||
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
|
||||
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
|
||||
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
|
||||
this.waInstances[instance]?.client?.ws?.close();
|
||||
this.waInstances[instance]?.client?.end(undefined);
|
||||
}
|
||||
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
||||
} else {
|
||||
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
||||
}
|
||||
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
||||
} else {
|
||||
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
||||
}
|
||||
} finally {
|
||||
// Clean up timeout reference
|
||||
delete this.delInstanceTimeouts[instance];
|
||||
}
|
||||
},
|
||||
1000 * 60 * time,
|
||||
@ -64,6 +76,13 @@ export class WAMonitoringService {
|
||||
}
|
||||
}
|
||||
|
||||
public clearDelInstanceTime(instance: string) {
|
||||
if (this.delInstanceTimeouts[instance]) {
|
||||
clearTimeout(this.delInstanceTimeouts[instance]);
|
||||
delete this.delInstanceTimeouts[instance];
|
||||
}
|
||||
}
|
||||
|
||||
public async instanceInfo(instanceNames?: string[]): Promise<any> {
|
||||
if (instanceNames && instanceNames.length > 0) {
|
||||
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
|
||||
@ -271,9 +290,19 @@ export class WAMonitoringService {
|
||||
token: instanceData.token,
|
||||
number: instanceData.number,
|
||||
businessId: instanceData.businessId,
|
||||
ownerJid: instanceData.ownerJid,
|
||||
});
|
||||
|
||||
await instance.connectToWhatsapp();
|
||||
if (instanceData.connectionStatus === 'open' || instanceData.connectionStatus === 'connecting') {
|
||||
this.logger.info(
|
||||
`Auto-connecting instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus})`,
|
||||
);
|
||||
await instance.connectToWhatsapp();
|
||||
} else {
|
||||
this.logger.info(
|
||||
`Skipping auto-connect for instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus || 'close'})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.waInstances[instanceData.instanceName] = instance;
|
||||
}
|
||||
@ -299,6 +328,7 @@ export class WAMonitoringService {
|
||||
token: instanceData.token,
|
||||
number: instanceData.number,
|
||||
businessId: instanceData.businessId,
|
||||
connectionStatus: instanceData.connectionStatus as any, // Pass connection status
|
||||
};
|
||||
|
||||
this.setInstance(instance);
|
||||
@ -327,6 +357,8 @@ export class WAMonitoringService {
|
||||
token: instance.token,
|
||||
number: instance.number,
|
||||
businessId: instance.businessId,
|
||||
ownerJid: instance.ownerJid,
|
||||
connectionStatus: instance.connectionStatus as any, // Pass connection status
|
||||
});
|
||||
}),
|
||||
);
|
||||
@ -351,6 +383,7 @@ export class WAMonitoringService {
|
||||
integration: instance.integration,
|
||||
token: instance.token,
|
||||
businessId: instance.businessId,
|
||||
connectionStatus: instance.connectionStatus as any, // Pass connection status
|
||||
});
|
||||
}),
|
||||
);
|
||||
@ -361,6 +394,8 @@ export class WAMonitoringService {
|
||||
try {
|
||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.REMOVE_INSTANCE, null);
|
||||
|
||||
this.clearDelInstanceTime(instanceName);
|
||||
|
||||
this.cleaningUp(instanceName);
|
||||
this.cleaningStoreData(instanceName);
|
||||
} finally {
|
||||
@ -377,6 +412,8 @@ export class WAMonitoringService {
|
||||
try {
|
||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
||||
|
||||
this.clearDelInstanceTime(instanceName);
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
this.waInstances[instanceName]?.clearCacheChatwoot();
|
||||
}
|
||||
|
||||
@ -88,6 +88,77 @@ export class TemplateService {
|
||||
}
|
||||
}
|
||||
|
||||
public async edit(
|
||||
instance: InstanceDto,
|
||||
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
|
||||
) {
|
||||
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
|
||||
if (!getInstance) {
|
||||
throw new Error('Instance not found');
|
||||
}
|
||||
|
||||
this.businessId = getInstance.businessId;
|
||||
this.token = getInstance.token;
|
||||
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (typeof data.category === 'string') payload.category = data.category;
|
||||
if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
|
||||
if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
|
||||
if (data.components) payload.components = data.components;
|
||||
|
||||
const response = await this.requestEditTemplate(data.templateId, payload);
|
||||
|
||||
if (!response || response.error) {
|
||||
if (response && response.error) {
|
||||
const metaError = new Error(response.error.message || 'WhatsApp API Error');
|
||||
(metaError as any).template = response.error;
|
||||
throw metaError;
|
||||
}
|
||||
throw new Error('Error to edit template');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async delete(instance: InstanceDto, data: { name: string; hsmId?: string }) {
|
||||
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
|
||||
if (!getInstance) {
|
||||
throw new Error('Instance not found');
|
||||
}
|
||||
|
||||
this.businessId = getInstance.businessId;
|
||||
this.token = getInstance.token;
|
||||
|
||||
const response = await this.requestDeleteTemplate({ name: data.name, hsm_id: data.hsmId });
|
||||
|
||||
if (!response || response.error) {
|
||||
if (response && response.error) {
|
||||
const metaError = new Error(response.error.message || 'WhatsApp API Error');
|
||||
(metaError as any).template = response.error;
|
||||
throw metaError;
|
||||
}
|
||||
throw new Error('Error to delete template');
|
||||
}
|
||||
|
||||
try {
|
||||
// Best-effort local cleanup of stored template metadata
|
||||
await this.prismaRepository.template.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ name: data.name, instanceId: getInstance.id },
|
||||
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
|
||||
].filter(Boolean) as any,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private async requestTemplate(data: any, method: string) {
|
||||
try {
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
@ -116,4 +187,38 @@ export class TemplateService {
|
||||
throw new Error(`Connection error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async requestEditTemplate(templateId: string, data: any) {
|
||||
try {
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${templateId}`;
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.post(urlServer, data, { headers });
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
|
||||
);
|
||||
if (e.response?.data) return e.response.data;
|
||||
throw new Error(`Connection error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async requestDeleteTemplate(params: { name: string; hsm_id?: string }) {
|
||||
try {
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`;
|
||||
const headers = { Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.delete(urlServer, { headers, params });
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
|
||||
);
|
||||
if (e.response?.data) return e.response.data;
|
||||
throw new Error(`Connection error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ export declare namespace wa {
|
||||
pairingCode?: string;
|
||||
authState?: { state: AuthenticationState; saveCreds: () => void };
|
||||
name?: string;
|
||||
ownerJid?: string;
|
||||
wuid?: string;
|
||||
profileName?: string;
|
||||
profilePictureUrl?: string;
|
||||
|
||||
@ -26,8 +26,8 @@ import cors from 'cors';
|
||||
import express, { json, NextFunction, Request, Response, urlencoded } from 'express';
|
||||
import { join } from 'path';
|
||||
|
||||
function initWA() {
|
||||
waMonitor.loadInstance();
|
||||
async function initWA() {
|
||||
await waMonitor.loadInstance();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
@ -159,7 +159,9 @@ async function bootstrap() {
|
||||
|
||||
server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT));
|
||||
|
||||
initWA();
|
||||
initWA().catch((error) => {
|
||||
logger.error('Error loading instances: ' + error);
|
||||
});
|
||||
|
||||
onUnexpectedError();
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { socksDispatcher } from 'fetch-socks';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import { ProxyAgent } from 'undici';
|
||||
@ -18,12 +19,23 @@ function selectProxyAgent(proxyUrl: string): HttpsProxyAgent<string> | SocksProx
|
||||
// the end so, we add the protocol constants without the `:` to avoid confusion.
|
||||
const PROXY_HTTP_PROTOCOL = 'http:';
|
||||
const PROXY_SOCKS_PROTOCOL = 'socks:';
|
||||
const PROXY_SOCKS5_PROTOCOL = 'socks5:';
|
||||
|
||||
switch (url.protocol) {
|
||||
case PROXY_HTTP_PROTOCOL:
|
||||
return new HttpsProxyAgent(url);
|
||||
case PROXY_SOCKS_PROTOCOL:
|
||||
return new SocksProxyAgent(url);
|
||||
case PROXY_SOCKS5_PROTOCOL: {
|
||||
let urlSocks = '';
|
||||
|
||||
if (url.username && url.password) {
|
||||
urlSocks = `socks://${url.username}:${url.password}@${url.hostname}:${url.port}`;
|
||||
} else {
|
||||
urlSocks = `socks://${url.hostname}:${url.port}`;
|
||||
}
|
||||
|
||||
return new SocksProxyAgent(urlSocks);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported proxy protocol: ${url.protocol}`);
|
||||
}
|
||||
@ -64,6 +76,8 @@ export function makeProxyAgentUndici(proxy: Proxy | string): ProxyAgent {
|
||||
proxyUrl = `${protocol}://${auth}${host}:${port}`;
|
||||
}
|
||||
|
||||
protocol = protocol.toLowerCase();
|
||||
|
||||
const PROXY_HTTP_PROTOCOL = 'http';
|
||||
const PROXY_HTTPS_PROTOCOL = 'https';
|
||||
const PROXY_SOCKS4_PROTOCOL = 'socks4';
|
||||
@ -72,10 +86,25 @@ export function makeProxyAgentUndici(proxy: Proxy | string): ProxyAgent {
|
||||
switch (protocol) {
|
||||
case PROXY_HTTP_PROTOCOL:
|
||||
case PROXY_HTTPS_PROTOCOL:
|
||||
case PROXY_SOCKS4_PROTOCOL:
|
||||
case PROXY_SOCKS5_PROTOCOL:
|
||||
return new ProxyAgent(proxyUrl);
|
||||
|
||||
case PROXY_SOCKS4_PROTOCOL:
|
||||
case PROXY_SOCKS5_PROTOCOL: {
|
||||
let type: 4 | 5 = 5;
|
||||
|
||||
if (PROXY_SOCKS4_PROTOCOL === protocol) type = 4;
|
||||
|
||||
const url = new URL(proxyUrl);
|
||||
|
||||
return socksDispatcher({
|
||||
type: type,
|
||||
host: url.hostname,
|
||||
port: Number(url.port),
|
||||
userId: url.username || undefined,
|
||||
password: url.password || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported proxy protocol: ${protocol}`);
|
||||
}
|
||||
|
||||
@ -65,78 +65,118 @@ interface ISaveOnWhatsappCacheParams {
|
||||
lid?: 'lid' | undefined;
|
||||
}
|
||||
|
||||
function normalizeJid(jid: string | null | undefined): string | null {
|
||||
if (!jid) return null;
|
||||
return jid.startsWith('+') ? jid.slice(1) : jid;
|
||||
}
|
||||
|
||||
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
||||
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
||||
for (const item of data) {
|
||||
const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
|
||||
if (!configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Processa todos os itens em paralelo para melhor performance
|
||||
const processingPromises = data.map(async (item) => {
|
||||
try {
|
||||
const remoteJid = normalizeJid(item.remoteJid);
|
||||
if (!remoteJid) {
|
||||
logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.');
|
||||
return;
|
||||
}
|
||||
|
||||
const expandedJids = allJids.flatMap((jid) => getAvailableNumbers(jid));
|
||||
const altJidNormalized = normalizeJid(item.remoteJidAlt);
|
||||
const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null;
|
||||
|
||||
const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial
|
||||
if (lidAltJid) {
|
||||
baseJids.push(lidAltJid);
|
||||
}
|
||||
|
||||
const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid));
|
||||
|
||||
// 1. Busca entrada por jidOptions e também remoteJid
|
||||
// Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro:
|
||||
// 'Unique constraint failed on the fields: (`remoteJid`)'
|
||||
// Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us')
|
||||
const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({
|
||||
where: {
|
||||
OR: expandedJids.map((jid) => ({ jidOptions: { contains: jid } })),
|
||||
OR: [
|
||||
...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })),
|
||||
{ remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
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}`,
|
||||
`[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`,
|
||||
);
|
||||
|
||||
// 2. Unifica todos os JIDs usando um Set para garantir valores únicos
|
||||
const finalJidOptions = new Set(expandedJids);
|
||||
|
||||
if (lidAltJid) {
|
||||
finalJidOptions.add(lidAltJid);
|
||||
}
|
||||
|
||||
if (existingRecord?.jidOptions) {
|
||||
existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid));
|
||||
}
|
||||
|
||||
// 3. Prepara o payload final
|
||||
// Ordena os JIDs para garantir consistência na string final
|
||||
const sortedJidOptions = [...finalJidOptions].sort();
|
||||
const newJidOptionsString = sortedJidOptions.join(',');
|
||||
const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null;
|
||||
|
||||
const dataPayload = {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: newJidOptionsString,
|
||||
lid: newLid,
|
||||
};
|
||||
|
||||
// 4. Decide entre Criar ou Atualizar
|
||||
if (existingRecord) {
|
||||
// Compara a string de JIDs ordenada existente com a nova
|
||||
const existingJidOptionsString = existingRecord.jidOptions
|
||||
? existingRecord.jidOptions.split(',').sort().join(',')
|
||||
: '';
|
||||
|
||||
const isDataSame =
|
||||
existingRecord.remoteJid === dataPayload.remoteJid &&
|
||||
existingJidOptionsString === dataPayload.jidOptions &&
|
||||
existingRecord.lid === dataPayload.lid;
|
||||
|
||||
if (isDataSame) {
|
||||
logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`);
|
||||
return; // Pula para o próximo item
|
||||
}
|
||||
|
||||
// Os dados são diferentes, então atualiza
|
||||
logger.verbose(
|
||||
`[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
||||
);
|
||||
await prismaRepository.isOnWhatsapp.update({
|
||||
where: { id: existingRecord.id },
|
||||
data: {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: uniqueNumbers.join(','),
|
||||
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
|
||||
},
|
||||
data: dataPayload,
|
||||
});
|
||||
} else {
|
||||
// Cria nova entrada
|
||||
logger.verbose(
|
||||
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
||||
);
|
||||
await prismaRepository.isOnWhatsapp.create({
|
||||
data: {
|
||||
remoteJid: remoteJid,
|
||||
jidOptions: uniqueNumbers.join(','),
|
||||
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
|
||||
},
|
||||
data: dataPayload,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Loga o erro mas não para a execução dos outros promises
|
||||
logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `);
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Espera todas as operações paralelas terminarem
|
||||
await Promise.allSettled(processingPromises);
|
||||
}
|
||||
|
||||
export async function getOnWhatsappCache(remoteJids: string[]) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { prismaRepository } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { CacheConf, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { INSTANCE_DIR } from '@config/path.config';
|
||||
import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys';
|
||||
import fs from 'fs/promises';
|
||||
@ -73,12 +74,15 @@ async function fileExists(file: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger('useMultiFileAuthStatePrisma');
|
||||
|
||||
export default async function useMultiFileAuthStatePrisma(
|
||||
sessionId: string,
|
||||
cache: CacheService,
|
||||
): Promise<{
|
||||
state: AuthenticationState;
|
||||
saveCreds: () => Promise<void>;
|
||||
removeCreds: () => Promise<void>;
|
||||
}> {
|
||||
const localFolder = path.join(INSTANCE_DIR, sessionId);
|
||||
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
|
||||
@ -142,6 +146,26 @@ export default async function useMultiFileAuthStatePrisma(
|
||||
}
|
||||
}
|
||||
|
||||
async function removeCreds(): Promise<any> {
|
||||
const cacheConfig = configService.get<CacheConf>('CACHE');
|
||||
|
||||
// Redis
|
||||
try {
|
||||
if (cacheConfig.REDIS.ENABLED) {
|
||||
await cache.delete(sessionId);
|
||||
logger.info({ action: 'redis.delete', sessionId });
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({ action: 'redis.delete', sessionId, err });
|
||||
}
|
||||
|
||||
logger.info({ action: 'auth.key.delete', sessionId });
|
||||
|
||||
await deleteAuthKey(sessionId);
|
||||
}
|
||||
|
||||
let creds = await readData('creds');
|
||||
if (!creds) {
|
||||
creds = initAuthCreds();
|
||||
@ -183,5 +207,7 @@ export default async function useMultiFileAuthStatePrisma(
|
||||
saveCreds: () => {
|
||||
return writeData(creds, 'creds');
|
||||
},
|
||||
|
||||
removeCreds,
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,7 +39,11 @@ import { Logger } from '@config/logger.config';
|
||||
import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
|
||||
import { isNotEmpty } from 'class-validator';
|
||||
|
||||
export type AuthState = { state: AuthenticationState; saveCreds: () => Promise<void> };
|
||||
export type AuthState = {
|
||||
state: AuthenticationState;
|
||||
saveCreds: () => Promise<void>;
|
||||
removeCreds: () => Promise<void>;
|
||||
};
|
||||
|
||||
export class AuthStateProvider {
|
||||
constructor(private readonly providerFiles: ProviderFiles) {}
|
||||
@ -86,6 +90,18 @@ export class AuthStateProvider {
|
||||
return response;
|
||||
};
|
||||
|
||||
const removeCreds = async () => {
|
||||
const [response, error] = await this.providerFiles.removeSession(instance);
|
||||
if (error) {
|
||||
// this.logger.error(['removeData', error?.message, error?.stack]);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ action: 'remove.session', instance, response });
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
||||
|
||||
return {
|
||||
@ -126,6 +142,10 @@ export class AuthStateProvider {
|
||||
saveCreds: async () => {
|
||||
return await writeData(creds, 'creds');
|
||||
},
|
||||
|
||||
removeCreds,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger('useMultiFileAuthStatePrisma');
|
||||
|
||||
@ -8,6 +8,7 @@ export async function useMultiFileAuthStateRedisDb(
|
||||
): Promise<{
|
||||
state: AuthenticationState;
|
||||
saveCreds: () => Promise<void>;
|
||||
removeCreds: () => Promise<void>;
|
||||
}> {
|
||||
const logger = new Logger('useMultiFileAuthStateRedisDb');
|
||||
|
||||
@ -36,6 +37,16 @@ export async function useMultiFileAuthStateRedisDb(
|
||||
}
|
||||
};
|
||||
|
||||
async function removeCreds(): Promise<any> {
|
||||
try {
|
||||
logger.warn({ action: 'redis.delete', instanceName });
|
||||
|
||||
return await cache.delete(instanceName);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
||||
|
||||
return {
|
||||
@ -76,5 +87,7 @@ export async function useMultiFileAuthStateRedisDb(
|
||||
saveCreds: async () => {
|
||||
return await writeData(creds, 'creds');
|
||||
},
|
||||
|
||||
removeCreds,
|
||||
};
|
||||
}
|
||||
|
||||
@ -195,8 +195,9 @@ export const contactValidateSchema: JSONSchema7 = {
|
||||
_id: { type: 'string', minLength: 1 },
|
||||
pushName: { type: 'string', minLength: 1 },
|
||||
id: { type: 'string', minLength: 1 },
|
||||
remoteJid: { type: 'string', minLength: 1 },
|
||||
},
|
||||
...isNotEmpty('_id', 'id', 'pushName'),
|
||||
...isNotEmpty('_id', 'id', 'pushName', 'remoteJid'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
32
src/validate/templateDelete.schema.ts
Normal file
32
src/validate/templateDelete.schema.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties: Record<string, unknown> = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
} as JSONSchema7;
|
||||
};
|
||||
|
||||
export const templateDeleteSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
hsmId: { type: 'string' },
|
||||
},
|
||||
required: ['name'],
|
||||
...isNotEmpty('name'),
|
||||
};
|
||||
35
src/validate/templateEdit.schema.ts
Normal file
35
src/validate/templateEdit.schema.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties: Record<string, unknown> = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
} as JSONSchema7;
|
||||
};
|
||||
|
||||
export const templateEditSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
templateId: { type: 'string' },
|
||||
category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
|
||||
allowCategoryChange: { type: 'boolean' },
|
||||
ttl: { type: 'number' },
|
||||
components: { type: 'array' },
|
||||
},
|
||||
required: ['templateId'],
|
||||
...isNotEmpty('templateId'),
|
||||
};
|
||||
@ -8,5 +8,7 @@ export * from './message.schema';
|
||||
export * from './proxy.schema';
|
||||
export * from './settings.schema';
|
||||
export * from './template.schema';
|
||||
export * from './templateDelete.schema';
|
||||
export * from './templateEdit.schema';
|
||||
export * from '@api/integrations/chatbot/chatbot.schema';
|
||||
export * from '@api/integrations/event/event.schema';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user