mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-27 15:47:45 -06:00
Compare commits
50 Commits
13f96a366b
...
4f642e17a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f642e17a7 | ||
|
|
afa6d633c6 | ||
|
|
2e3e752719 | ||
|
|
de11e6f9ca | ||
|
|
26e7eefe51 | ||
|
|
b55c9fcab7 | ||
|
|
86b194af5f | ||
|
|
3c1573c400 | ||
|
|
178386594c | ||
|
|
cea1fa0979 | ||
|
|
38be0b49d9 | ||
|
|
04ac880fcc | ||
|
|
3864366e75 | ||
|
|
2756d7e61c | ||
|
|
bb36bfe424 | ||
|
|
6277c5d084 | ||
|
|
b1d77019f5 | ||
|
|
8d5c7d875e | ||
|
|
abd0351f8f | ||
|
|
c7a2aa51ee | ||
|
|
bbf60e30b0 | ||
|
|
2408384b0f | ||
|
|
250ddd2e89 | ||
|
|
bee309cd28 | ||
|
|
92c2ace7bc | ||
|
|
faed3f4574 | ||
|
|
baff4e8f5e | ||
|
|
1c3a7ab027 | ||
|
|
338cc93cfc | ||
|
|
930d32df3a | ||
|
|
fa6b5c28a6 | ||
|
|
8e7f348c12 | ||
|
|
5c58cb7eae | ||
|
|
879bee962b | ||
|
|
af47b859e4 | ||
|
|
1c61116a3e | ||
|
|
08a4795016 | ||
|
|
53a94af3f7 | ||
|
|
302e219f7f | ||
|
|
1e036ba3ae | ||
|
|
377993e4b0 | ||
|
|
ea88edd512 | ||
|
|
d3e3c458a0 | ||
|
|
067f0999b5 | ||
|
|
179af3f41c | ||
|
|
31a6f2d92e | ||
|
|
dc72f01625 | ||
|
|
3b139078c3 | ||
|
|
f2c2a6a64a | ||
|
|
e5a249109c |
85
CHANGELOG.md
85
CHANGELOG.md
@@ -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
11
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user