Compare commits

...

50 Commits

Author SHA1 Message Date
Davidson Gomes
4f642e17a7 chore: changelog v2.3.7
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
2025-12-05 11:28:40 -03:00
Davidson Gomes
afa6d633c6 chore(changelog): update version 2.3.7 with latest features and fixes 2025-12-05 11:13:17 -03:00
Davidson Gomes
2e3e752719 style(whatsapp): fix indentation and remove unnecessary blank lines in BaileysStartupService 2025-12-05 11:03:52 -03:00
Davidson Gomes
de11e6f9ca fix(websocket): improve host validation logic in WebsocketController 2025-12-05 11:03:52 -03:00
Davidson Gomes
26e7eefe51 Merge pull request #2259 from muriloleal13/fix/baileys-message-stub-placeholder
fix(baileys): prevent message loss from WhatsApp stub placeholders
2025-12-05 11:02:34 -03:00
Davidson Gomes
b55c9fcab7 Merge pull request #2250 from gabrielmouallem/fix/respect-database-save-data-contacts
fix: respect DATABASE_SAVE_DATA_CONTACTS in contact updates
2025-12-05 11:01:46 -03:00
Davidson Gomes
86b194af5f Merge pull request #2260 from alexandrereyes/feat/add-islatest-progress-to-messages-set
feat(events): add isLatest and progress to messages.set event
2025-12-05 11:01:21 -03:00
Davidson Gomes
3c1573c400 Merge pull request #2238 from jamesjhonatan123/feature/quote-message-n8n
Feature/quote message n8n
2025-12-05 11:00:54 -03:00
Davidson Gomes
178386594c Merge branch 'develop' into feature/quote-message-n8n 2025-12-05 11:00:40 -03:00
Davidson Gomes
cea1fa0979 Merge pull request #2247 from msantosjader/fix/postgres-chat-constraint
fix(prisma): add unique constraint to Chat model in Postgres
2025-12-05 10:59:22 -03:00
Davidson Gomes
38be0b49d9 Merge pull request #2280 from micaelmz/feature/wildcard-for-websocket-allowed-hosts
feat: add wildcard "*" to allow all hosts to connect via websocket
2025-12-05 10:59:03 -03:00
Alexandre Martins
04ac880fcc style: fix lint formatting issues 2025-12-05 10:58:42 -03:00
Davidson Gomes
3864366e75 Merge pull request #2273 from kay0ramon/fix/minio-messagecontextinfo-upload-error
fix: handle messageContextInfo in media upload to prevent MinIO errors
2025-12-05 10:57:55 -03:00
Davidson Gomes
2756d7e61c Merge pull request #2264 from lucascampuus/patch-1
Fix Typebot message routing for @lid JIDs
2025-12-05 10:57:31 -03:00
Davidson Gomes
bb36bfe424 Merge pull request #2249 from rodps/fix/fetch-messages-jid
fix: unify remoteJid filtering using OR with remoteJidAlt
2025-12-05 10:55:50 -03:00
Davidson Gomes
6277c5d084 Merge branch 'develop' into patch-1 2025-12-05 10:55:05 -03:00
Davidson Gomes
b1d77019f5 Merge pull request #2275 from Vitordotpy/fix/all-wrong-things-in-this-api
Fix: @lid problems, messages events and chatwoot integration errors
2025-12-05 10:51:49 -03:00
Davidson Gomes
8d5c7d875e Merge pull request #2240 from JefersonRamos/bugfix/media-upload-failed-on-all-hosts
Bugfix/media upload failed on all hosts
2025-12-05 10:48:04 -03:00
micaelmz
abd0351f8f feat: add wildcard "*" to allow all hosts to connect via websocket 2025-12-02 18:01:19 -03:00
Vitordotpy
c7a2aa51ee fix: reorganize imports and improve message handling in BaileysStartupService 2025-11-30 19:56:03 -03:00
Vitor Manoel Santos Moura
bbf60e30b0 Refactor imports and clean up code structure 2025-11-30 18:51:34 -03:00
Vitor Manoel Santos Moura
2408384b0f Refactor message handling and polling updates
Refactor message handling and polling updates, including decryption logic for poll votes and cache management for message updates. Improved event processing flow and added handling for various message types.
2025-11-30 00:25:17 -03:00
Vitordotpy
250ddd2e89 fix(chatwoot): improve jid normalization and type safety in chatwoot integration
Refactor  to preserve LID identifiers and update  parameter type for better type safety as per code review feedback.
2025-11-28 21:28:45 -03:00
Vitordotpy
bee309cd28 fix: streamline message handling logic and improve cache management in BaileysStartupService 2025-11-28 21:14:19 -03:00
Vitordotpy
92c2ace7bc fix: enhance remoteJid processing to handle '@lid' cases 2025-11-28 19:03:24 -03:00
Vitordotpy
faed3f4574 fix: improve error handling for existing contacts and simplify remoteJid processing 2025-11-28 16:32:06 -03:00
Vitordotpy
baff4e8f5e fix: update remoteJid handling to avoid unnecessary splitting for message number 2025-11-28 16:18:33 -03:00
Kayo Ramon Oliveira
1c3a7ab027 fix: handle messageContextInfo in media upload to prevent MinIO errors 2025-11-28 15:59:09 -03:00
Lucas Luiz Campos
338cc93cfc Fix Typebot message routing for @lid JIDs
O Typebot não respondia mensagens vindas de JIDs que terminam com "@lid", apenas "@s.whatsapp.net".

O comportamento ocorria porque o número era sempre extraído via:
remoteJid.split('@')[0]

Com a atualização do WhatsApp Web, algumas mensagens de mídia chegam com JID "@lid", e nesses casos o JID completo precisa ser mantido.

Ajuste realizado:
ANTES:
number: remoteJid.split('@')[0]

DEPOIS:
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0]

Com essa condição, mensagens vindas de ambos os formatos passam a ser tratadas corretamente pelo Typebot.
2025-11-27 09:31:40 -03:00
Alexandre Martins
930d32df3a fix(events): guard extra spread and prevent core field override
- Use (extra ?? {}) to handle undefined extra safely
- Spread extra first to prevent overriding core fields like event, instance, data
- Applied fix to all 7 event controllers

Addresses Sourcery AI review feedback.
2025-11-26 15:48:53 -03:00
Alexandre Martins
fa6b5c28a6 feat(events): add isLatest and progress to messages.set event
- Add extra field to EmitData type for additional payload properties
- Update EventManager and sendDataWebhook to support extra parameters
- Update all event controllers (webhook, rabbitmq, sqs, websocket, pusher, kafka, nats) to include extra fields in payload
- Pass isLatest and progress from Baileys messaging-history.set to messages.set webhook

This allows consumers to know when the history sync is complete (isLatest=true) and track sync progress percentage.
2025-11-26 15:44:18 -03:00
Murilo Leal
8e7f348c12 fix(baileys): prevent message loss from WhatsApp stub placeholders
Mensagens do WhatsApp estavam sendo perdidas e não eram salvas no banco de dados, especialmente mensagens de canais/newsletters (@lid) e mensagens com criptografia complexa.

O WhatsApp/Baileys envia mensagens criptografadas em duas etapas:

1. Primeiro: Envia um stub (placeholder) com messageStubParameters: ['Message absent from node'] enquanto descriptografa a mensagem

2. Depois: Envia a mensagem real com o conteúdo descriptografado

O problema ocorria porque:

- O stub chegava primeiro e era adicionado ao cache de mensagens duplicadas

- O stub era descartado (corretamente) por não ter conteúdo (!received?.message)

- A mensagem real chegava depois, mas era ignorada como duplicata porque o ID já estava no cache

- Resultado: mensagem nunca era salva no banco de dados

Solução:

- Detectar stubs do WhatsApp através de messageStubParameters contendo 'Message absent from node'

- Não adicionar stubs ao cache de mensagens duplicadas

- Permitir que a mensagem real seja processada quando chegar

- Manter o descarte do stub para evitar salvar placeholders vazios
2025-11-26 13:29:31 -03:00
Jeferson Ramos
5c58cb7eae lint 2025-11-24 14:19:48 -03:00
Jeferson Ramos
879bee962b lint 2025-11-24 14:17:27 -03:00
Jeferson Ramos
af47b859e4 socks5 update 2025-11-24 13:59:50 -03:00
Jeferson Ramos
1c61116a3e Merge remote-tracking branch 'upstream/develop' into bugfix/media-upload-failed-on-all-hosts
# Conflicts:
#	package-lock.json
#	src/utils/makeProxyAgent.ts
2025-11-24 13:58:25 -03:00
Gabriel Mouallem
08a4795016 fix: respect DATABASE_SAVE_DATA_CONTACTS in contact updates
- Added missing conditional checks for `DATABASE_SAVE_DATA_CONTACTS` in `contacts.upsert` and `contacts.update` handlers.
- Fixed an issue where profile picture updates were attempting to save to the database even when disabled.
- Fixed an unawaited promise in `contacts.upsert` to ensure database operations complete correctly.
2025-11-23 23:09:42 -03:00
Gabriel Mouallem
53a94af3f7 fix: respect DATABASE_SAVE_DATA_CONTACTS in contact updates 2025-11-23 22:59:18 -03:00
Rodrigo da Silva
302e219f7f fix: unify remoteJid filtering using OR with remoteJidAlt 2025-11-23 18:46:06 -03:00
Jader Santos
1e036ba3ae fix(migration): add deduplication step before creating index 2025-11-21 22:09:15 -03:00
Jader Santos
377993e4b0 fix(prisma): add unique constraint to Chat model in Postgres
Generated migration to add unique index on instanceId and remoteJid.
2025-11-21 21:40:27 -03:00
Jeferson Ramos
ea88edd512 socks 2025-11-19 16:51:59 -03:00
Jeferson Ramos
d3e3c458a0 lint 2025-11-19 14:09:07 -03:00
Jeferson Ramos
067f0999b5 lint 2025-11-19 14:07:23 -03:00
Jeferson Ramos
179af3f41c lint 2025-11-19 14:02:52 -03:00
Jeferson Ramos
31a6f2d92e ; 2025-11-19 14:00:17 -03:00
Jeferson Ramos
dc72f01625 Merge remote-tracking branch 'upstream/main' into bugfix/media-upload-failed-on-all-hosts 2025-11-19 13:34:47 -03:00
Jeferson Ramos
3b139078c3 Removendo uso do undici com proxy socks 2025-11-19 13:34:33 -03:00
Jonatas
f2c2a6a64a refactor: improve formatting and consistency in makeProxyAgent functions 2025-11-18 23:58:18 -03:00
Jonatas
e5a249109c feat: add quotedMessage to payload in sendMessageToBot on N8N 2025-11-18 23:52:36 -03:00
22 changed files with 570 additions and 170 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,4 +1,4 @@
# 2.3.7 (develop)
# 2.3.7 (2025-12-05)
### Features
@@ -7,6 +7,24 @@
- Added DTOs and validation schemas for template management
- Enhanced template lifecycle management capabilities
* **Events API**: Add isLatest and progress to messages.set event
- Allows consumers to know when history sync is complete (isLatest=true)
- Track sync progress percentage through webhooks
- Added extra field to EmitData type for additional payload properties
- Updated all event controllers (webhook, rabbitmq, sqs, websocket, pusher, kafka, nats)
* **N8N Integration**: Add quotedMessage to payload in sendMessageToBot
- Support for quoted messages in N8N chatbot integration
- Enhanced message context information
* **WebSocket**: Add wildcard "*" to allow all hosts to connect via websocket
- More flexible host configuration for WebSocket connections
- Improved host validation logic in WebsocketController
* **Pix Support**: Handle interactive button message for pix
- Support for interactive Pix button messages
- Enhanced payment flow integration
### Fixed
* **Baileys Message Processor**: Fix incoming message events not working after reconnection
@@ -60,11 +78,76 @@
- Fixed integration issues between Chatwoot and Baileys services
- Improved message handling and delivery
* **Baileys Message Loss**: Prevent message loss from WhatsApp stub placeholders
- Fixed messages being lost and not saved to database, especially for channels/newsletters (@lid)
- Detects WhatsApp stubs through messageStubParameters containing 'Message absent from node'
- Prevents adding stubs to duplicate message cache
- Allows real message to be processed when it arrives after decryption
- Maintains stub discard to avoid saving empty placeholders
* **Database Contacts**: Respect DATABASE_SAVE_DATA_CONTACTS in contact updates
- Added missing conditional checks for DATABASE_SAVE_DATA_CONTACTS configuration
- Fixed profile picture updates attempting to save when database save is disabled
- Fixed unawaited promise in contacts.upsert handler
* **Prisma/PostgreSQL**: Add unique constraint to Chat model
- Generated migration to add unique index on instanceId and remoteJid
- Added deduplication step before creating index to prevent constraint violations
- Prevents chat duplication in database
* **MinIO Upload**: Handle messageContextInfo in media upload to prevent MinIO errors
- Prevents errors when uploading media with messageContextInfo metadata
- Improved error handling for media storage operations
* **Typebot**: Fix message routing for @lid JIDs
- Typebot now responds to messages from JIDs ending with @lid
- Maintains complete JID for @lid instead of extracting only number
- Fixed condition: `remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0]`
- Handles both @s.whatsapp.net and @lid message formats
* **Message Filtering**: Unify remoteJid filtering using OR with remoteJidAlt
- Improved message filtering with alternative JID support
- Better handling of messages with different JID formats
* **@lid Integration**: Multiple fixes for @lid problems, message events and chatwoot errors
- Reorganized imports and improved message handling in BaileysStartupService
- Enhanced remoteJid processing to handle @lid cases
- Improved jid normalization and type safety in Chatwoot integration
- Streamlined message handling logic and cache management
- Refactored message handling and polling updates with decryption logic for poll votes
- Improved event processing flow for various message types
* **Chatwoot Contacts**: Fix contact duplication error on import
- Resolved 'ON CONFLICT DO UPDATE command cannot affect row a second time' error
- Removed attempt to update identifier field in conflict (part of constraint)
- Changed to update only updated_at field: `updated_at = NOW()`
- Allows duplicate contacts to be updated correctly without errors
* **Chatwoot Service**: Fix async handling in update_last_seen method
- Added missing await for chatwootRequest in read message processing
- Prevents service failure when processing read messages
* **Metrics Access**: Fix IP validation including x-forwarded-for
- Uses all IPs including x-forwarded-for header when checking metrics access
- Improved security and access control for metrics endpoint
### Dependencies
* **Baileys**: Updated to version 7.0.0-rc.9
- Latest release candidate with multiple improvements and bug fixes
* **AWS SDK**: Updated packages to version 3.936.0
- Enhanced functionality and compatibility
- Performance improvements
### 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
* **BaileysStartupService**: Fix indentation and remove unnecessary blank lines
* **Event Controllers**: Guard extra spread and prevent core field override in all event controllers
* **Import Organization**: Reorganize imports for better code structure and maintainability
# 2.3.6 (2025-10-21)

11
package-lock.json generated
View File

@@ -31,6 +31,7 @@
"eventemitter2": "^6.4.9",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"fetch-socks": "^1.3.2",
"fluent-ffmpeg": "^2.1.3",
"form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6",
@@ -8578,6 +8579,16 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-socks": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fetch-socks/-/fetch-socks-1.3.2.tgz",
"integrity": "sha512-vkH5+Zgj2yEbU57Cei0iyLgTZ4OkEKJj56Xu3ViB5dpsl599JgEooQ3x6NVagIFRHWnWJ+7K0MO0aIV1TMgvnw==",
"license": "MIT",
"dependencies": {
"socks": "^2.8.2",
"undici": ">=6"
}
},
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",

View File

@@ -90,6 +90,7 @@
"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",

View File

@@ -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");

View File

@@ -132,6 +132,7 @@ model Chat {
instanceId String
unreadMessages Int @default(0)
@@unique([instanceId, remoteJid])
@@index([instanceId])
@@index([remoteJid])
}

View File

@@ -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
@@ -857,10 +861,12 @@ export class BaileysStartupService extends ChannelStartupService {
this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts);
await Promise.all(
updatedContacts.map(async (contact) => {
const update = this.prismaRepository.contact.updateMany({
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
data: { profilePicUrl: contact.profilePicUrl },
});
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
await this.prismaRepository.contact.updateMany({
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
data: { profilePicUrl: contact.profilePicUrl },
});
}
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
const instance = { instanceName: this.instance.name, instanceId: this.instance.id };
@@ -879,8 +885,6 @@ export class BaileysStartupService extends ChannelStartupService {
avatar_url: contact.profilePicUrl,
});
}
return update;
}),
);
}
@@ -904,14 +908,16 @@ export class BaileysStartupService extends ChannelStartupService {
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw);
const updateTransactions = contactsRaw.map((contact) =>
this.prismaRepository.contact.upsert({
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
create: contact,
update: contact,
}),
);
await this.prismaRepository.$transaction(updateTransactions);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
const updateTransactions = contactsRaw.map((contact) =>
this.prismaRepository.contact.upsert({
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
create: contact,
update: contact,
}),
);
await this.prismaRepository.$transaction(updateTransactions);
}
//const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
},
@@ -1040,7 +1046,10 @@ export class BaileysStartupService extends ChannelStartupService {
messagesRaw.push(this.prepareMessage(m));
}
this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]);
this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, {
isLatest,
progress,
});
if (this.configService.get<Database>('DATABASE').SAVE_DATA.HISTORIC) {
await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true });
@@ -1086,6 +1095,7 @@ export class BaileysStartupService extends ChannelStartupService {
'Invalid PreKey ID',
'No session record',
'No session found to decrypt message',
'Message absent from node',
].some((err) => param?.includes?.(err)),
)
) {
@@ -1121,6 +1131,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)
@@ -1148,12 +1163,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (
(type !== 'notify' && type !== 'append') ||
editedMessage ||
received.message?.pollUpdateMessage ||
!received?.message
) {
if ((type !== 'notify' && type !== 'append') || editedMessage || !received?.message) {
continue;
}
@@ -1193,6 +1203,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 ||
@@ -1242,7 +1353,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;
@@ -1290,6 +1403,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(
@@ -1447,18 +1565,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) {
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) {
@@ -1489,19 +1615,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;
@@ -1514,7 +1653,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 });
@@ -1562,8 +1701,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 },
@@ -1614,9 +1756,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 {
@@ -1732,135 +1874,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;
}
}
});
});
}
@@ -2326,6 +2474,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();
@@ -3699,7 +3852,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;
@@ -4882,7 +5036,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: [
@@ -4912,7 +5065,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: [

View File

@@ -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,

View File

@@ -346,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;
@@ -415,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);
@@ -1574,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}`);
}
}
}
@@ -2024,7 +2087,7 @@ 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.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();
@@ -2206,7 +2269,7 @@ 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.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();
@@ -2464,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];
}

View File

@@ -51,6 +51,7 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
pushName: pushName,
keyId: msg?.key?.id,
fromMe: msg?.key?.fromMe,
quotedMessage: msg?.contextInfo?.quotedMessage,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,

View File

@@ -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: '',

View File

@@ -14,12 +14,24 @@ export type EmitData = {
apiKey?: string;
local?: boolean;
integration?: string[];
extra?: Record<string, any>;
};
export interface EventControllerInterface {
set(instanceName: string, data: any): Promise<any>;
get(instanceName: string): Promise<any>;
emit({ instanceName, origin, event, data, serverUrl, dateTime, sender, apiKey, local }: EmitData): Promise<void>;
emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
local,
extra,
}: EmitData): Promise<void>;
}
export class EventController {

View File

@@ -123,6 +123,7 @@ export class EventManager {
apiKey?: string;
local?: boolean;
integration?: string[];
extra?: Record<string, any>;
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);

View File

@@ -262,6 +262,7 @@ export class KafkaController extends EventController implements EventControllerI
sender,
apiKey,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('kafka')) {
return;
@@ -284,6 +285,7 @@ export class KafkaController extends EventController implements EventControllerI
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -47,6 +47,7 @@ export class NatsController extends EventController implements EventControllerIn
sender,
apiKey,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('nats')) {
return;
@@ -65,6 +66,7 @@ export class NatsController extends EventController implements EventControllerIn
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -121,6 +121,7 @@ export class PusherController extends EventController implements EventController
apiKey,
local,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('pusher')) {
return;
@@ -133,6 +134,7 @@ export class PusherController extends EventController implements EventController
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const eventName = event.replace(/_/g, '.').toLowerCase();
const pusherData = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -209,6 +209,7 @@ export class RabbitmqController extends EventController implements EventControll
sender,
apiKey,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('rabbitmq')) {
return;
@@ -233,6 +234,7 @@ export class RabbitmqController extends EventController implements EventControll
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -93,6 +93,7 @@ export class SqsController extends EventController implements EventControllerInt
sender,
apiKey,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('sqs')) {
return;
@@ -128,6 +129,7 @@ export class SqsController extends EventController implements EventControllerInt
const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`;
const message = {
...(extra ?? {}),
event,
instance: instanceName,
dataType: 'json',

View File

@@ -65,6 +65,7 @@ export class WebhookController extends EventController implements EventControlle
apiKey,
local,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('webhook')) {
return;
@@ -90,6 +91,7 @@ export class WebhookController extends EventController implements EventControlle
const regex = /^(https?:\/\/)/;
const webhookData = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -33,10 +33,13 @@ 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
.split(',')
.map((h) => h.trim())
.includes(remoteAddress);
const allowAllHosts = allowedHosts.trim() === '*';
const isAllowedHost =
allowAllHosts ||
allowedHosts
.split(',')
.map((h) => h.trim())
.includes(remoteAddress);
if (params.has('EIO') && isAllowedHost) {
return callback(null, true);
@@ -115,6 +118,7 @@ export class WebsocketController extends EventController implements EventControl
sender,
apiKey,
integration,
extra,
}: EmitData): Promise<void> {
if (integration && !integration.includes('websocket')) {
return;
@@ -127,6 +131,7 @@ export class WebsocketController extends EventController implements EventControl
const configEv = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET');
const message = {
...(extra ?? {}),
event,
instance: instanceName,
data,

View File

@@ -432,7 +432,13 @@ export class ChannelStartupService {
return data;
}
public async sendDataWebhook<T extends object = any>(event: Events, data: T, local = true, integration?: string[]) {
public async sendDataWebhook<T extends object = any>(
event: Events,
data: T,
local = true,
integration?: string[],
extra?: Record<string, any>,
) {
const serverUrl = this.configService.get<HttpServer>('SERVER').URL;
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
@@ -453,6 +459,7 @@ export class ChannelStartupService {
apiKey: expose && instanceApikey ? instanceApikey : null,
local,
integration,
extra,
});
}

View File

@@ -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}`);
}