mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-12 19:39:36 -06:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd800f2976 | ||
|
|
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 | ||
|
|
13f96a366b | ||
|
|
08a4795016 | ||
|
|
53a94af3f7 | ||
|
|
302e219f7f | ||
|
|
1e036ba3ae | ||
|
|
377993e4b0 | ||
|
|
689f347457 | ||
|
|
7743063439 | ||
|
|
f5e43a3b3f | ||
|
|
ea88edd512 | ||
|
|
e6a9ed92ce | ||
|
|
8707520a3e | ||
|
|
d3e3c458a0 | ||
|
|
067f0999b5 | ||
|
|
179af3f41c | ||
|
|
31a6f2d92e | ||
|
|
dc72f01625 | ||
|
|
3b139078c3 | ||
|
|
f2c2a6a64a | ||
|
|
e5a249109c | ||
|
|
73fb376602 | ||
|
|
27633aad53 | ||
|
|
06543e89e5 | ||
|
|
90640b7cee | ||
|
|
da8774caa2 | ||
|
|
4ae3139163 | ||
|
|
f4043a9141 | ||
|
|
139ad9b3cb | ||
|
|
fca39a2b34 | ||
|
|
1e3a23588e | ||
|
|
27be03ea95 | ||
|
|
9b73252f35 | ||
|
|
71322cd8f6 | ||
|
|
263854db47 | ||
|
|
400b6291a2 | ||
|
|
feff038446 | ||
|
|
4d2a189905 | ||
|
|
48625a739c | ||
|
|
b6620d2bd6 | ||
|
|
45e461e757 | ||
|
|
be5760905e | ||
|
|
92626fa559 | ||
|
|
1aaad541ad | ||
|
|
3b0432dd9f | ||
|
|
a95c843e77 | ||
|
|
8d1151d0a0 | ||
|
|
a1393b679c | ||
|
|
5cbc163716 | ||
|
|
a84faaa575 | ||
|
|
503cbfb21c | ||
|
|
40281871c8 | ||
|
|
85868b3439 | ||
|
|
066e060b86 | ||
|
|
c555048783 | ||
|
|
2d14c8849b | ||
|
|
df20c5fc93 | ||
|
|
3818313161 | ||
|
|
4a38e505f4 | ||
|
|
d5f5b8325e | ||
|
|
1ad51a434b | ||
|
|
3454bec79f | ||
|
|
8c27f11f5b | ||
|
|
ae9f3efeff | ||
|
|
ba3a2fae59 | ||
|
|
aa0d793d26 | ||
|
|
48bda1b5af | ||
|
|
dd21a29ea6 | ||
|
|
e83a7e2e88 | ||
|
|
d58d0b8bff | ||
|
|
4efc9b65bc | ||
|
|
cd71ff503d | ||
|
|
582166e5ae | ||
|
|
e1ae03c1e4 | ||
|
|
0737c45df2 | ||
|
|
adbe1079d5 | ||
|
|
423f629b04 | ||
|
|
946dcaeb2e | ||
|
|
f0c6300599 | ||
|
|
24c339343f | ||
|
|
ddbaf2335a | ||
|
|
20c8a2ff0e | ||
|
|
e623269a18 | ||
|
|
0363fa979d | ||
|
|
b640329cf8 | ||
|
|
f72b1f7717 | ||
|
|
ed4c8868a0 | ||
|
|
06081f6502 | ||
|
|
8e51ae63ae |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -59,7 +59,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
- OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0]
|
- OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0]
|
||||||
- Node.js version: [e.g. 18.17.0]
|
- Node.js version: [e.g. 18.17.0]
|
||||||
- Evolution API version: [e.g. 2.3.5]
|
- Evolution API version: [e.g. 2.3.7]
|
||||||
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
|
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
|
||||||
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
- Connection type: [e.g. Baileys, WhatsApp Business API]
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
179
CHANGELOG.md
179
CHANGELOG.md
@ -1,3 +1,182 @@
|
|||||||
|
# 2.3.7 (2025-12-05)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **WhatsApp Business Meta Templates**: Add update and delete endpoints for Meta templates
|
||||||
|
- New endpoints to edit and delete WhatsApp Business templates
|
||||||
|
- Added DTOs and validation schemas for template management
|
||||||
|
- Enhanced template lifecycle management capabilities
|
||||||
|
|
||||||
|
* **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
|
||||||
|
- Added cleanup logic in mount() to prevent memory leaks from multiple subscriptions
|
||||||
|
- Recreate messageSubject if it was completed during logout
|
||||||
|
- Remount messageProcessor in connectToWhatsapp() to ensure subscription is active
|
||||||
|
- Fixed issue where onDestroy() calls complete() on RxJS Subject, making it permanently closed
|
||||||
|
- Ensures old subscriptions are properly cleaned up before creating new ones
|
||||||
|
|
||||||
|
* **Baileys Authentication**: Resolve "waiting for message" state after reconnection
|
||||||
|
- Fixed Redis keys not being properly removed during instance logout
|
||||||
|
- Prevented loading of old/invalid cryptographic keys on reconnection
|
||||||
|
- Fixed blocking state where instances authenticate but cannot send messages
|
||||||
|
- Ensures new credentials (creds) are properly used after reconnection
|
||||||
|
|
||||||
|
* **OnWhatsapp Cache**: Prevent unique constraint errors and optimize database writes
|
||||||
|
- Fixed `Unique constraint failed on the fields: (remoteJid)` error when sending to groups
|
||||||
|
- Refactored query to use OR condition finding by jidOptions or remoteJid
|
||||||
|
- Added deep comparison to skip unnecessary database updates
|
||||||
|
- Replaced sequential processing with Promise.allSettled for parallel execution
|
||||||
|
- Sorted JIDs alphabetically in jidOptions for accurate change detection
|
||||||
|
- Added normalizeJid helper function for cleaner code
|
||||||
|
|
||||||
|
* **Proxy Integration**: Fix "Media upload failed on all hosts" error when using proxy
|
||||||
|
- Created makeProxyAgentUndici() for Undici-compatible proxy agents
|
||||||
|
- Fixed compatibility with Node.js 18+ native fetch() implementation
|
||||||
|
- Replaced traditional HttpsProxyAgent/SocksProxyAgent with Undici ProxyAgent
|
||||||
|
- Maintained legacy makeProxyAgent() for Axios compatibility
|
||||||
|
- Fixed protocol handling in makeProxyAgent to prevent undefined errors
|
||||||
|
|
||||||
|
* **WhatsApp Business API**: Fix base64, filename and caption handling
|
||||||
|
- Corrected base64 media conversion in Business API
|
||||||
|
- Fixed filename handling for document messages
|
||||||
|
- Improved caption processing for media messages
|
||||||
|
- Enhanced remoteJid validation and processing
|
||||||
|
|
||||||
|
* **Chat Service**: Fix fetchChats and message panel errors
|
||||||
|
- Fixed cleanMessageData errors in Manager message panel
|
||||||
|
- Improved chat fetching reliability
|
||||||
|
- Enhanced message data sanitization
|
||||||
|
|
||||||
|
* **Contact Filtering**: Apply where filters correctly in findContacts endpoint
|
||||||
|
- Fixed endpoint to process all where clause fields (id, remoteJid, pushName)
|
||||||
|
- Previously only processed remoteJid field, ignoring other filters
|
||||||
|
- Added remoteJid field to contactValidateSchema for proper validation
|
||||||
|
- Maintained multi-tenant isolation with instanceId filtering
|
||||||
|
- Allows filtering contacts by any supported field instead of returning all contacts
|
||||||
|
|
||||||
|
* **Chatwoot and Baileys Integration**: Multiple integration improvements
|
||||||
|
- Enhanced code formatting and consistency
|
||||||
|
- Fixed integration issues between Chatwoot and Baileys services
|
||||||
|
- Improved message handling and delivery
|
||||||
|
|
||||||
|
* **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)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Baileys, Chatwoot, OnWhatsapp Cache**: Multiple implementations and fixes
|
||||||
|
- Fixed cache for PN, LID and g.us numbers to send correct number
|
||||||
|
- Fixed audio and document sending via Chatwoot in Baileys channel
|
||||||
|
- Multiple fixes in Chatwoot integration
|
||||||
|
- Fixed ignored messages when receiving leads
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* **Baileys**: Fix buffer storage in database
|
||||||
|
- Correctly save Uint8Array values to database
|
||||||
|
* **Baileys**: Simplify logging of messageSent object
|
||||||
|
- Fixed "this.isZero not is function" error
|
||||||
|
|
||||||
|
### Chore
|
||||||
|
|
||||||
|
* **Version**: Bump version to 2.3.6 and update Baileys dependency to 7.0.0-rc.6
|
||||||
|
* **Workflows**: Update checkout step to include submodules
|
||||||
|
- Added 'submodules: recursive' option to checkout step in multiple workflow files to ensure submodules are properly initialized during CI/CD processes
|
||||||
|
* **Manager**: Update asset files and install process
|
||||||
|
- Updated subproject reference in evolution-manager-v2 to the latest commit
|
||||||
|
- Enhanced the manager_install.sh script to include npm install and build steps
|
||||||
|
- Replaced old JavaScript asset file with a new version for improved performance
|
||||||
|
- Added a new CSS file for consistent styling across the application
|
||||||
|
|
||||||
# 2.3.5 (2025-10-15)
|
# 2.3.5 (2025-10-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@ -2,7 +2,7 @@ version: "3.7"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
evolution_v2:
|
evolution_v2:
|
||||||
image: evoapicloud/evolution-api:v2.3.5
|
image: evoapicloud/evolution-api:v2.3.7
|
||||||
volumes:
|
volumes:
|
||||||
- evolution_instances:/evolution/instances
|
- evolution_instances:/evolution/instances
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
4933
package-lock.json
generated
4933
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.3.5",
|
"version": "2.3.7",
|
||||||
"description": "Rest api for communication with WhatsApp",
|
"description": "Rest api for communication with WhatsApp",
|
||||||
"main": "./dist/main.js",
|
"main": "./dist/main.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
@ -77,7 +77,7 @@
|
|||||||
"amqplib": "^0.10.5",
|
"amqplib": "^0.10.5",
|
||||||
"audio-decode": "^2.2.3",
|
"audio-decode": "^2.2.3",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"baileys": "^7.0.0-rc.5",
|
"baileys": "7.0.0-rc.9",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.7.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@ -90,12 +90,14 @@
|
|||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"form-data": "^4.0.1",
|
"form-data": "^4.0.1",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"fetch-socks": "^1.3.2",
|
||||||
"i18next": "^23.7.19",
|
"i18next": "^23.7.19",
|
||||||
"jimp": "^1.6.0",
|
"jimp": "^1.6.0",
|
||||||
"json-schema": "^0.4.0",
|
"json-schema": "^0.4.0",
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
|
"libphonenumber-js": "^1.12.25",
|
||||||
"link-preview-js": "^3.0.13",
|
"link-preview-js": "^3.0.13",
|
||||||
"long": "^5.2.3",
|
"long": "^5.2.3",
|
||||||
"mediainfo.js": "^0.3.4",
|
"mediainfo.js": "^0.3.4",
|
||||||
@ -121,6 +123,7 @@
|
|||||||
"socks-proxy-agent": "^8.0.5",
|
"socks-proxy-agent": "^8.0.5",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tsup": "^8.3.5",
|
"tsup": "^8.3.5",
|
||||||
|
"undici": "^7.16.0",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -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
|
instanceId String
|
||||||
unreadMessages Int @default(0)
|
unreadMessages Int @default(0)
|
||||||
|
|
||||||
|
@@unique([instanceId, remoteJid])
|
||||||
@@index([instanceId])
|
@@index([instanceId])
|
||||||
@@index([remoteJid])
|
@@index([remoteJid])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,6 +92,15 @@ export class InstanceController {
|
|||||||
instanceId: instanceId,
|
instanceId: instanceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const instanceDto: InstanceDto = {
|
||||||
|
instanceName: instance.instanceName,
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
connectionStatus:
|
||||||
|
typeof instance.connectionStatus === 'string'
|
||||||
|
? instance.connectionStatus
|
||||||
|
: instance.connectionStatus?.state || 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
|
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
|
||||||
const testProxy = await this.proxyService.testProxy({
|
const testProxy = await this.proxyService.testProxy({
|
||||||
host: instanceData.proxyHost,
|
host: instanceData.proxyHost,
|
||||||
@ -103,8 +112,7 @@ export class InstanceController {
|
|||||||
if (!testProxy) {
|
if (!testProxy) {
|
||||||
throw new BadRequestException('Invalid proxy');
|
throw new BadRequestException('Invalid proxy');
|
||||||
}
|
}
|
||||||
|
await this.proxyService.createProxy(instanceDto, {
|
||||||
await this.proxyService.createProxy(instance, {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
host: instanceData.proxyHost,
|
host: instanceData.proxyHost,
|
||||||
port: instanceData.proxyPort,
|
port: instanceData.proxyPort,
|
||||||
@ -125,7 +133,7 @@ export class InstanceController {
|
|||||||
wavoipToken: instanceData.wavoipToken || '',
|
wavoipToken: instanceData.wavoipToken || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.settingsService.create(instance, settings);
|
await this.settingsService.create(instanceDto, settings);
|
||||||
|
|
||||||
let webhookWaBusiness = null,
|
let webhookWaBusiness = null,
|
||||||
accessTokenWaBusiness = '';
|
accessTokenWaBusiness = '';
|
||||||
@ -155,7 +163,10 @@ export class InstanceController {
|
|||||||
integration: instanceData.integration,
|
integration: instanceData.integration,
|
||||||
webhookWaBusiness,
|
webhookWaBusiness,
|
||||||
accessTokenWaBusiness,
|
accessTokenWaBusiness,
|
||||||
status: instance.connectionStatus.state,
|
status:
|
||||||
|
typeof instance.connectionStatus === 'string'
|
||||||
|
? instance.connectionStatus
|
||||||
|
: instance.connectionStatus?.state || 'unknown',
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
webhook: {
|
webhook: {
|
||||||
@ -217,7 +228,7 @@ export class InstanceController {
|
|||||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.chatwootService.create(instance, {
|
this.chatwootService.create(instanceDto, {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
accountId: instanceData.chatwootAccountId,
|
accountId: instanceData.chatwootAccountId,
|
||||||
token: instanceData.chatwootToken,
|
token: instanceData.chatwootToken,
|
||||||
@ -246,7 +257,10 @@ export class InstanceController {
|
|||||||
integration: instanceData.integration,
|
integration: instanceData.integration,
|
||||||
webhookWaBusiness,
|
webhookWaBusiness,
|
||||||
accessTokenWaBusiness,
|
accessTokenWaBusiness,
|
||||||
status: instance.connectionStatus.state,
|
status:
|
||||||
|
typeof instance.connectionStatus === 'string'
|
||||||
|
? instance.connectionStatus
|
||||||
|
: instance.connectionStatus?.state || 'unknown',
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
webhook: {
|
webhook: {
|
||||||
@ -338,20 +352,38 @@ export class InstanceController {
|
|||||||
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
|
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == 'close') {
|
if (state === 'close') {
|
||||||
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
|
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
|
||||||
} else if (state == 'open') {
|
}
|
||||||
|
this.logger.info(`Restarting instance: ${instanceName}`);
|
||||||
|
|
||||||
|
if (typeof instance.restart === 'function') {
|
||||||
|
await instance.restart();
|
||||||
|
// Wait a bit for the reconnection to be established
|
||||||
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
|
return {
|
||||||
|
instance: {
|
||||||
|
instanceName: instanceName,
|
||||||
|
status: instance.connectionStatus?.state || 'connecting',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for Baileys (uses different mechanism)
|
||||||
|
if (state === 'open' || state === 'connecting') {
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
|
||||||
this.logger.info('restarting instance' + instanceName);
|
|
||||||
|
|
||||||
instance.client?.ws?.close();
|
|
||||||
instance.client?.end(new Error('restart'));
|
|
||||||
return await this.connectToWhatsapp({ instanceName });
|
|
||||||
} else if (state == 'connecting') {
|
|
||||||
instance.client?.ws?.close();
|
instance.client?.ws?.close();
|
||||||
instance.client?.end(new Error('restart'));
|
instance.client?.end(new Error('restart'));
|
||||||
return await this.connectToWhatsapp({ instanceName });
|
return await this.connectToWhatsapp({ instanceName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance: {
|
||||||
|
instanceName: instanceName,
|
||||||
|
status: state,
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
return { error: true, message: error.toString() };
|
return { error: true, message: error.toString() };
|
||||||
@ -409,7 +441,7 @@ export class InstanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.waMonitor.waInstances[instanceName]?.logoutInstance();
|
await this.waMonitor.waInstances[instanceName]?.logoutInstance();
|
||||||
|
|
||||||
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
|
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -12,4 +12,15 @@ export class TemplateController {
|
|||||||
public async findTemplate(instance: InstanceDto) {
|
public async findTemplate(instance: InstanceDto) {
|
||||||
return this.templateService.find(instance);
|
return this.templateService.find(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async editTemplate(
|
||||||
|
instance: InstanceDto,
|
||||||
|
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
|
||||||
|
) {
|
||||||
|
return this.templateService.edit(instance, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteTemplate(instance: InstanceDto, data: { name: string; hsmId?: string }) {
|
||||||
|
return this.templateService.delete(instance, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export class InstanceDto extends IntegrationDto {
|
|||||||
token?: string;
|
token?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
ownerJid?: string;
|
ownerJid?: string;
|
||||||
|
connectionStatus?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
profilePicUrl?: string;
|
profilePicUrl?: string;
|
||||||
// settings
|
// settings
|
||||||
|
|||||||
@ -6,3 +6,16 @@ export class TemplateDto {
|
|||||||
components: any;
|
components: any;
|
||||||
webhookUrl?: string;
|
webhookUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TemplateEditDto {
|
||||||
|
templateId: string;
|
||||||
|
category?: 'AUTHENTICATION' | 'MARKETING' | 'UTILITY';
|
||||||
|
allowCategoryChange?: boolean;
|
||||||
|
ttl?: number;
|
||||||
|
components?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TemplateDeleteDto {
|
||||||
|
name: string;
|
||||||
|
hsmId?: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -516,7 +516,9 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
|
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
messageRaw.message.mediaUrl = mediaUrl;
|
||||||
|
if (this.localWebhook.enabled && this.localWebhook.webhookBase64) {
|
||||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
||||||
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
||||||
@ -554,11 +556,19 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (this.localWebhook.enabled && this.localWebhook.webhookBase64) {
|
||||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||||
messageRaw.message.base64 = buffer.toString('base64');
|
messageRaw.message.base64 = buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
||||||
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
||||||
|
let openAiBase64 = messageRaw.message.base64;
|
||||||
|
if (!openAiBase64) {
|
||||||
|
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||||
|
openAiBase64 = buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||||
where: {
|
where: {
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
@ -574,7 +584,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
openAiDefaultSettings.OpenaiCreds,
|
openAiDefaultSettings.OpenaiCreds,
|
||||||
{
|
{
|
||||||
message: {
|
message: {
|
||||||
base64: messageRaw.message.base64,
|
base64: openAiBase64,
|
||||||
...messageRaw,
|
...messageRaw,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1016,6 +1026,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
[message['mediaType']]: {
|
[message['mediaType']]: {
|
||||||
[message['type']]: message['id'],
|
[message['type']]: message['id'],
|
||||||
...(message['mediaType'] !== 'audio' &&
|
...(message['mediaType'] !== 'audio' &&
|
||||||
|
message['mediaType'] !== 'video' &&
|
||||||
message['fileName'] &&
|
message['fileName'] &&
|
||||||
!isImage && { filename: message['fileName'] }),
|
!isImage && { filename: message['fileName'] }),
|
||||||
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
||||||
@ -1606,9 +1617,14 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
|
const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
|
||||||
const mediaMessage = msg.message[messageType];
|
const mediaMessage = msg.message[messageType];
|
||||||
|
|
||||||
|
if (!msg.message?.base64) {
|
||||||
|
const buffer = await this.downloadMediaMessage({ type: messageType, ...msg.message });
|
||||||
|
msg.message.base64 = buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaType: msg.messageType,
|
mediaType: msg.messageType,
|
||||||
fileName: mediaMessage?.fileName,
|
fileName: mediaMessage?.fileName || mediaMessage?.filename,
|
||||||
caption: mediaMessage?.caption,
|
caption: mediaMessage?.caption,
|
||||||
size: {
|
size: {
|
||||||
fileLength: mediaMessage?.fileLength,
|
fileLength: mediaMessage?.fileLength,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Logger } from '@config/logger.config';
|
import { Logger } from '@config/logger.config';
|
||||||
import { BaileysEventMap, MessageUpsertType, proto } from 'baileys';
|
import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys';
|
||||||
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
|
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
|
||||||
|
|
||||||
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
||||||
@ -12,13 +12,29 @@ export class BaileysMessageProcessor {
|
|||||||
private subscription?: Subscription;
|
private subscription?: Subscription;
|
||||||
|
|
||||||
protected messageSubject = new Subject<{
|
protected messageSubject = new Subject<{
|
||||||
messages: proto.IWebMessageInfo[];
|
messages: WAMessage[];
|
||||||
type: MessageUpsertType;
|
type: MessageUpsertType;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
settings: any;
|
settings: any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
mount({ onMessageReceive }: MountProps) {
|
mount({ onMessageReceive }: MountProps) {
|
||||||
|
// Se já existe subscription, fazer cleanup primeiro
|
||||||
|
if (this.subscription && !this.subscription.closed) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se o Subject foi completado, recriar
|
||||||
|
if (this.messageSubject.closed) {
|
||||||
|
this.processorLogs.warn('MessageSubject was closed, recreating...');
|
||||||
|
this.messageSubject = new Subject<{
|
||||||
|
messages: WAMessage[];
|
||||||
|
type: MessageUpsertType;
|
||||||
|
requestId?: string;
|
||||||
|
settings: any;
|
||||||
|
}>();
|
||||||
|
}
|
||||||
|
|
||||||
this.subscription = this.messageSubject
|
this.subscription = this.messageSubject
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(({ messages }) => {
|
tap(({ messages }) => {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ import { createId as cuid } from '@paralleldrive/cuid2';
|
|||||||
import { Instance, Message } from '@prisma/client';
|
import { Instance, Message } from '@prisma/client';
|
||||||
import { createJid } from '@utils/createJid';
|
import { createJid } from '@utils/createJid';
|
||||||
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
||||||
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent';
|
||||||
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
||||||
import { status } from '@utils/renderStatus';
|
import { status } from '@utils/renderStatus';
|
||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
@ -99,6 +99,7 @@ import makeWASocket, {
|
|||||||
Chat,
|
Chat,
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
Contact,
|
Contact,
|
||||||
|
decryptPollVote,
|
||||||
delay,
|
delay,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
downloadContentFromMessage,
|
downloadContentFromMessage,
|
||||||
@ -113,6 +114,7 @@ import makeWASocket, {
|
|||||||
isJidGroup,
|
isJidGroup,
|
||||||
isJidNewsletter,
|
isJidNewsletter,
|
||||||
isPnUser,
|
isPnUser,
|
||||||
|
jidNormalizedUser,
|
||||||
makeCacheableSignalKeyStore,
|
makeCacheableSignalKeyStore,
|
||||||
MessageUpsertType,
|
MessageUpsertType,
|
||||||
MessageUserReceiptUpdate,
|
MessageUserReceiptUpdate,
|
||||||
@ -133,7 +135,7 @@ import { Label } from 'baileys/lib/Types/Label';
|
|||||||
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { isArray, isBase64, isURL } from 'class-validator';
|
import { isArray, isBase64, isURL } from 'class-validator';
|
||||||
import { randomBytes } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
@ -248,6 +250,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
|
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
|
||||||
private endSession = false;
|
private endSession = false;
|
||||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||||
|
private eventProcessingQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
// Cache TTL constants (in seconds)
|
// Cache TTL constants (in seconds)
|
||||||
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
|
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
|
||||||
@ -267,6 +270,28 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.client?.ws?.close();
|
this.client?.ws?.close();
|
||||||
|
|
||||||
|
const db = this.configService.get<Database>('DATABASE');
|
||||||
|
const cache = this.configService.get<CacheConf>('CACHE');
|
||||||
|
const provider = this.configService.get<ProviderSession>('PROVIDER');
|
||||||
|
|
||||||
|
if (provider?.ENABLED) {
|
||||||
|
const authState = await this.authStateProvider.authStateProvider(this.instance.id);
|
||||||
|
|
||||||
|
await authState.removeCreds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) {
|
||||||
|
const authState = await useMultiFileAuthStateRedisDb(this.instance.id, this.cache);
|
||||||
|
|
||||||
|
await authState.removeCreds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.SAVE_DATA.INSTANCE) {
|
||||||
|
const authState = await useMultiFileAuthStatePrisma(this.instance.id, this.cache);
|
||||||
|
|
||||||
|
await authState.removeCreds();
|
||||||
|
}
|
||||||
|
|
||||||
const sessionExists = await this.prismaRepository.session.findFirst({ where: { sessionId: this.instanceId } });
|
const sessionExists = await this.prismaRepository.session.findFirst({ where: { sessionId: this.instanceId } });
|
||||||
if (sessionExists) {
|
if (sessionExists) {
|
||||||
await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId } });
|
await this.prismaRepository.session.delete({ where: { sessionId: this.instanceId } });
|
||||||
@ -570,15 +595,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
const version = baileysVersion.version;
|
const version = baileysVersion.version;
|
||||||
const log = `Baileys version: ${version.join('.')}`;
|
const log = `Baileys version: ${version.join('.')}`;
|
||||||
|
|
||||||
// if (session.VERSION) {
|
|
||||||
// version = session.VERSION.split('.');
|
|
||||||
// log = `Baileys version env: ${version}`;
|
|
||||||
// } else {
|
|
||||||
// const baileysVersion = await fetchLatestWaWebVersion({});
|
|
||||||
// version = baileysVersion.version;
|
|
||||||
// log = `Baileys version: ${version}`;
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.logger.info(log);
|
this.logger.info(log);
|
||||||
|
|
||||||
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
||||||
@ -595,7 +611,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
const proxyUrls = text.split('\r\n');
|
const proxyUrls = text.split('\r\n');
|
||||||
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
||||||
const proxyUrl = 'http://' + proxyUrls[rand];
|
const proxyUrl = 'http://' + proxyUrls[rand];
|
||||||
options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgent(proxyUrl) };
|
options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
|
||||||
} catch {
|
} catch {
|
||||||
this.localProxy.enabled = false;
|
this.localProxy.enabled = false;
|
||||||
}
|
}
|
||||||
@ -608,7 +624,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
username: this.localProxy.username,
|
username: this.localProxy.username,
|
||||||
password: this.localProxy.password,
|
password: this.localProxy.password,
|
||||||
}),
|
}),
|
||||||
fetchAgent: makeProxyAgent({
|
fetchAgent: makeProxyAgentUndici({
|
||||||
host: this.localProxy.host,
|
host: this.localProxy.host,
|
||||||
port: this.localProxy.port,
|
port: this.localProxy.port,
|
||||||
protocol: this.localProxy.protocol,
|
protocol: this.localProxy.protocol,
|
||||||
@ -711,6 +727,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.loadWebhook();
|
this.loadWebhook();
|
||||||
this.loadProxy();
|
this.loadProxy();
|
||||||
|
|
||||||
|
// Remontar o messageProcessor para garantir que está funcionando após reconexão
|
||||||
|
this.messageProcessor.mount({
|
||||||
|
onMessageReceive: this.messageHandle['messages.upsert'].bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
return await this.createClient(number);
|
return await this.createClient(number);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
@ -840,10 +861,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts);
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
updatedContacts.map(async (contact) => {
|
updatedContacts.map(async (contact) => {
|
||||||
const update = this.prismaRepository.contact.updateMany({
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
|
||||||
|
await this.prismaRepository.contact.updateMany({
|
||||||
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
|
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
|
||||||
data: { profilePicUrl: contact.profilePicUrl },
|
data: { profilePicUrl: contact.profilePicUrl },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||||
const instance = { instanceName: this.instance.name, instanceId: this.instance.id };
|
const instance = { instanceName: this.instance.name, instanceId: this.instance.id };
|
||||||
@ -862,8 +885,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
avatar_url: contact.profilePicUrl,
|
avatar_url: contact.profilePicUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return update;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -876,6 +897,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
||||||
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
|
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
|
||||||
for await (const contact of contacts) {
|
for await (const contact of contacts) {
|
||||||
|
this.logger.debug(`Updating contact: ${JSON.stringify(contact, null, 2)}`);
|
||||||
contactsRaw.push({
|
contactsRaw.push({
|
||||||
remoteJid: contact.id,
|
remoteJid: contact.id,
|
||||||
pushName: contact?.name ?? contact?.verifiedName,
|
pushName: contact?.name ?? contact?.verifiedName,
|
||||||
@ -886,6 +908,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw);
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw);
|
||||||
|
|
||||||
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
|
||||||
const updateTransactions = contactsRaw.map((contact) =>
|
const updateTransactions = contactsRaw.map((contact) =>
|
||||||
this.prismaRepository.contact.upsert({
|
this.prismaRepository.contact.upsert({
|
||||||
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
|
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
|
||||||
@ -894,11 +917,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await this.prismaRepository.$transaction(updateTransactions);
|
await this.prismaRepository.$transaction(updateTransactions);
|
||||||
|
|
||||||
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
|
||||||
if (usersContacts) {
|
|
||||||
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1025,7 +1046,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
messagesRaw.push(this.prepareMessage(m));
|
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) {
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.HISTORIC) {
|
||||||
await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true });
|
await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true });
|
||||||
@ -1071,6 +1095,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
'Invalid PreKey ID',
|
'Invalid PreKey ID',
|
||||||
'No session record',
|
'No session record',
|
||||||
'No session found to decrypt message',
|
'No session found to decrypt message',
|
||||||
|
'Message absent from node',
|
||||||
].some((err) => param?.includes?.(err)),
|
].some((err) => param?.includes?.(err)),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -1106,6 +1131,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
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);
|
const oldMessage = await this.getMessage(editedMessage.key, true);
|
||||||
if ((oldMessage as any)?.id) {
|
if ((oldMessage as any)?.id) {
|
||||||
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
|
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
|
||||||
@ -1133,22 +1163,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageKey = `${this.instance.id}_${received.key.id}`;
|
if ((type !== 'notify' && type !== 'append') || editedMessage || !received?.message) {
|
||||||
const cached = await this.baileysCache.get(messageKey);
|
|
||||||
|
|
||||||
if (cached && !editedMessage) {
|
|
||||||
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(type !== 'notify' && type !== 'append') ||
|
|
||||||
editedMessage ||
|
|
||||||
received.message?.pollUpdateMessage ||
|
|
||||||
!received?.message
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1188,6 +1203,107 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
const messageRaw = this.prepareMessage(received);
|
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 =
|
const isMedia =
|
||||||
received?.message?.imageMessage ||
|
received?.message?.imageMessage ||
|
||||||
received?.message?.videoMessage ||
|
received?.message?.videoMessage ||
|
||||||
@ -1237,7 +1353,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
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 { remoteJid } = received.key;
|
||||||
const timestamp = msg.messageTimestamp;
|
const timestamp = msg.messageTimestamp;
|
||||||
@ -1285,6 +1403,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
} else {
|
} else {
|
||||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
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 { buffer, mediaType, fileName, size } = media;
|
||||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||||
const fullName = join(
|
const fullName = join(
|
||||||
@ -1349,9 +1472,13 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.verbose(messageRaw);
|
||||||
|
|
||||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
|
||||||
|
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
|
||||||
|
}
|
||||||
|
console.log(messageRaw);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
@ -1366,7 +1493,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
|
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = {
|
const contactRaw: {
|
||||||
|
remoteJid: string;
|
||||||
|
pushName: string;
|
||||||
|
profilePicUrl?: string;
|
||||||
|
instanceId: string;
|
||||||
|
} = {
|
||||||
remoteJid: received.key.remoteJid,
|
remoteJid: received.key.remoteJid,
|
||||||
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
||||||
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
||||||
@ -1377,6 +1509,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contactRaw.remoteJid.includes('@s.whatsapp') || contactRaw.remoteJid.includes('@lid')) {
|
||||||
|
await saveOnWhatsappCache([
|
||||||
|
{
|
||||||
|
remoteJid:
|
||||||
|
messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid,
|
||||||
|
remoteJidAlt: messageRaw.key.remoteJidAlt,
|
||||||
|
lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
||||||
|
|
||||||
@ -1406,10 +1549,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
update: contactRaw,
|
update: contactRaw,
|
||||||
create: contactRaw,
|
create: contactRaw,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contactRaw.remoteJid.includes('@s.whatsapp')) {
|
|
||||||
await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
@ -1417,7 +1556,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
},
|
},
|
||||||
|
|
||||||
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
|
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
|
||||||
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
||||||
|
|
||||||
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
||||||
|
|
||||||
@ -1426,18 +1565,26 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.message !== null && update.status === undefined) continue;
|
|
||||||
|
|
||||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||||
|
|
||||||
const cached = await this.baileysCache.get(updateKey);
|
const cached = await this.baileysCache.get(updateKey);
|
||||||
|
|
||||||
if (cached) {
|
const secondsSinceEpoch = Math.floor(Date.now() / 1000);
|
||||||
this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
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;
|
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 (status[update.status] === 'READ' && key.fromMe) {
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||||
@ -1468,19 +1615,32 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
remoteJid: key?.remoteJid,
|
remoteJid: key?.remoteJid,
|
||||||
fromMe: key.fromMe,
|
fromMe: key.fromMe,
|
||||||
participant: key?.participant,
|
participant: key?.participant,
|
||||||
status: status[update.status] ?? 'DELETED',
|
status: status[update.status] ?? 'SERVER_ACK',
|
||||||
pollUpdates,
|
pollUpdates,
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (update.message) {
|
||||||
|
message.message = update.message;
|
||||||
|
}
|
||||||
|
|
||||||
let findMessage: any;
|
let findMessage: any;
|
||||||
const configDatabaseData = this.configService.get<Database>('DATABASE').SAVE_DATA;
|
const configDatabaseData = this.configService.get<Database>('DATABASE').SAVE_DATA;
|
||||||
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
|
||||||
// Use raw SQL to avoid JSON path issues
|
// 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`
|
const messages = (await this.prismaRepository.$queryRaw`
|
||||||
SELECT * FROM "Message"
|
SELECT * FROM "Message"
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${searchId}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`) as any[];
|
`) as any[];
|
||||||
findMessage = messages[0] || null;
|
findMessage = messages[0] || null;
|
||||||
@ -1493,7 +1653,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (update.message === null && update.status === undefined) {
|
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)
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
||||||
await this.prismaRepository.messageUpdate.create({ data: message });
|
await this.prismaRepository.messageUpdate.create({ data: message });
|
||||||
@ -1541,8 +1701,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
||||||
|
|
||||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
|
||||||
await this.prismaRepository.messageUpdate.create({ data: message });
|
// 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({
|
const existingChat = await this.prismaRepository.chat.findFirst({
|
||||||
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
||||||
@ -1593,9 +1756,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
// This enables LID to phoneNumber conversion without breaking existing webhook consumers
|
// This enables LID to phoneNumber conversion without breaking existing webhook consumers
|
||||||
|
|
||||||
// Helper to normalize participantId as phone number
|
// 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
|
// Remove @lid, @s.whatsapp.net suffixes and extract just the number part
|
||||||
return id.split('@')[0];
|
return String(id || '').split('@')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1711,6 +1874,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
private eventHandler() {
|
private eventHandler() {
|
||||||
this.client.ev.process(async (events) => {
|
this.client.ev.process(async (events) => {
|
||||||
|
this.eventProcessingQueue = this.eventProcessingQueue.then(async () => {
|
||||||
|
try {
|
||||||
if (!this.endSession) {
|
if (!this.endSession) {
|
||||||
const database = this.configService.get<Database>('DATABASE');
|
const database = this.configService.get<Database>('DATABASE');
|
||||||
const settings = await this.findSettings();
|
const settings = await this.findSettings();
|
||||||
@ -1744,19 +1909,19 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
if (events['messaging-history.set']) {
|
if (events['messaging-history.set']) {
|
||||||
const payload = events['messaging-history.set'];
|
const payload = events['messaging-history.set'];
|
||||||
this.messageHandle['messaging-history.set'](payload);
|
await this.messageHandle['messaging-history.set'](payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events['messages.upsert']) {
|
if (events['messages.upsert']) {
|
||||||
const payload = events['messages.upsert'];
|
const payload = events['messages.upsert'];
|
||||||
|
|
||||||
this.messageProcessor.processMessage(payload, settings);
|
// this.messageProcessor.processMessage(payload, settings);
|
||||||
// this.messageHandle['messages.upsert'](payload, settings);
|
await this.messageHandle['messages.upsert'](payload, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events['messages.update']) {
|
if (events['messages.update']) {
|
||||||
const payload = events['messages.update'];
|
const payload = events['messages.update'];
|
||||||
this.messageHandle['messages.update'](payload, settings);
|
await this.messageHandle['messages.update'](payload, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events['message-receipt.update']) {
|
if (events['message-receipt.update']) {
|
||||||
@ -1798,7 +1963,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (events['group-participants.update']) {
|
if (events['group-participants.update']) {
|
||||||
const payload = events['group-participants.update'];
|
const payload = events['group-participants.update'] as any;
|
||||||
this.groupHandler['group-participants.update'](payload);
|
this.groupHandler['group-participants.update'](payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1840,6 +2005,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1966,6 +2135,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
quoted: any,
|
quoted: any,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
ephemeralExpiration?: number,
|
ephemeralExpiration?: number,
|
||||||
|
contextInfo?: any,
|
||||||
// participants?: GroupParticipant[],
|
// participants?: GroupParticipant[],
|
||||||
) {
|
) {
|
||||||
sender = sender.toLowerCase();
|
sender = sender.toLowerCase();
|
||||||
@ -1982,8 +2152,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
|
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
|
||||||
|
|
||||||
|
// NOTE: NÃO DEVEMOS GERAR O messageId AQUI, SOMENTE SE VIER INFORMADO POR PARAMETRO. A GERAÇÃO ANTERIOR IMPEDE O WZAP DE IDENTIFICAR A SOURCE.
|
||||||
if (messageId) option.messageId = messageId;
|
if (messageId) option.messageId = messageId;
|
||||||
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
|
|
||||||
|
|
||||||
if (message['viewOnceMessage']) {
|
if (message['viewOnceMessage']) {
|
||||||
const m = generateWAMessageFromContent(sender, message, {
|
const m = generateWAMessageFromContent(sender, message, {
|
||||||
@ -2020,10 +2190,19 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contextInfo) {
|
||||||
|
message['contextInfo'] = contextInfo;
|
||||||
|
}
|
||||||
|
|
||||||
if (message['conversation']) {
|
if (message['conversation']) {
|
||||||
return await this.client.sendMessage(
|
return await this.client.sendMessage(
|
||||||
sender,
|
sender,
|
||||||
{ text: message['conversation'], mentions, linkPreview: linkPreview } as unknown as AnyMessageContent,
|
{
|
||||||
|
text: message['conversation'],
|
||||||
|
mentions,
|
||||||
|
linkPreview: linkPreview,
|
||||||
|
contextInfo: message['contextInfo'],
|
||||||
|
} as unknown as AnyMessageContent,
|
||||||
option as unknown as MiscMessageGenerationOptions,
|
option as unknown as MiscMessageGenerationOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2031,7 +2210,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
||||||
return await this.client.sendMessage(
|
return await this.client.sendMessage(
|
||||||
sender,
|
sender,
|
||||||
{ forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message }, mentions },
|
{
|
||||||
|
forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message },
|
||||||
|
mentions,
|
||||||
|
contextInfo: message['contextInfo'],
|
||||||
|
},
|
||||||
option as unknown as MiscMessageGenerationOptions,
|
option as unknown as MiscMessageGenerationOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2162,7 +2345,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (options?.quoted) {
|
if (options?.quoted) {
|
||||||
const m = options?.quoted;
|
const m = options?.quoted;
|
||||||
|
|
||||||
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo);
|
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as WAMessage);
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
quoted = msg;
|
quoted = msg;
|
||||||
@ -2172,6 +2355,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
let messageSent: WAMessage;
|
let messageSent: WAMessage;
|
||||||
|
|
||||||
let mentions: string[];
|
let mentions: string[];
|
||||||
|
let contextInfo: any;
|
||||||
|
|
||||||
if (isJidGroup(sender)) {
|
if (isJidGroup(sender)) {
|
||||||
let group;
|
let group;
|
||||||
try {
|
try {
|
||||||
@ -2210,7 +2395,27 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
// group?.participants,
|
// group?.participants,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted);
|
contextInfo = {
|
||||||
|
mentionedJid: [],
|
||||||
|
groupMentions: [],
|
||||||
|
//expiration: 7776000,
|
||||||
|
ephemeralSettingTimestamp: {
|
||||||
|
low: Math.floor(Date.now() / 1000) - 172800,
|
||||||
|
high: 0,
|
||||||
|
unsigned: false,
|
||||||
|
},
|
||||||
|
disappearingMode: { initiator: 0 },
|
||||||
|
};
|
||||||
|
messageSent = await this.sendMessage(
|
||||||
|
sender,
|
||||||
|
message,
|
||||||
|
mentions,
|
||||||
|
linkPreview,
|
||||||
|
quoted,
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
contextInfo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Long.isLong(messageSent?.messageTimestamp)) {
|
if (Long.isLong(messageSent?.messageTimestamp)) {
|
||||||
@ -2269,6 +2474,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
} else {
|
} else {
|
||||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
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 { buffer, mediaType, fileName, size } = media;
|
||||||
|
|
||||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||||
@ -2330,7 +2540,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.verbose(messageSent);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
||||||
|
|
||||||
@ -3337,42 +3547,55 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
|
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Separate @lid numbers from normal numbers
|
// Unified cache verification for all numbers (normal and LID)
|
||||||
const lidUsers = jids.users.filter(({ jid }) => jid.includes('@lid'));
|
const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', ''));
|
||||||
const normalUsers = jids.users.filter(({ jid }) => !jid.includes('@lid'));
|
|
||||||
|
|
||||||
// For normal numbers, use traditional Baileys verification
|
|
||||||
let normalVerifiedUsers: OnWhatsAppDto[] = [];
|
|
||||||
if (normalUsers.length > 0) {
|
|
||||||
console.log('normalUsers', normalUsers);
|
|
||||||
const numbersToVerify = normalUsers.map(({ jid }) => jid.replace('+', ''));
|
|
||||||
console.log('numbersToVerify', numbersToVerify);
|
|
||||||
|
|
||||||
|
// Get all numbers from cache
|
||||||
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
||||||
console.log('cachedNumbers', cachedNumbers);
|
|
||||||
|
|
||||||
const filteredNumbers = numbersToVerify.filter(
|
// Separate numbers that are and are not in cache
|
||||||
(jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)),
|
const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions));
|
||||||
);
|
const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid));
|
||||||
console.log('filteredNumbers', filteredNumbers);
|
|
||||||
|
|
||||||
const verify = await this.client.onWhatsApp(...filteredNumbers);
|
// Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache
|
||||||
console.log('verify', verify);
|
let verify: { jid: string; exists: boolean }[] = [];
|
||||||
normalVerifiedUsers = await Promise.all(
|
const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid'));
|
||||||
normalUsers.map(async (user) => {
|
|
||||||
let numberVerified: (typeof verify)[0] | null = null;
|
|
||||||
|
|
||||||
|
if (normalNumbersNotInCache.length > 0) {
|
||||||
|
this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`);
|
||||||
|
verify = await this.client.onWhatsApp(...normalNumbersNotInCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifiedUsers = await Promise.all(
|
||||||
|
jids.users.map(async (user) => {
|
||||||
|
// Try to get from cache first (works for all: normal and LID)
|
||||||
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
this.logger.verbose(`Number ${user.number} found in cache`);
|
||||||
return new OnWhatsAppDto(
|
return new OnWhatsAppDto(
|
||||||
cached.remoteJid,
|
cached.remoteJid,
|
||||||
true,
|
true,
|
||||||
user.number,
|
user.number,
|
||||||
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
|
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
|
||||||
cached.lid || (cached.remoteJid.includes('@lid') ? cached.remoteJid.split('@')[1] : undefined),
|
cached.lid || (cached.remoteJid.includes('@lid') ? 'lid' : undefined),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's a LID number and not in cache, consider it valid
|
||||||
|
if (user.jid.includes('@lid')) {
|
||||||
|
return new OnWhatsAppDto(
|
||||||
|
user.jid,
|
||||||
|
true,
|
||||||
|
user.number,
|
||||||
|
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
|
||||||
|
'lid',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache and is a normal number, use Baileys verification
|
||||||
|
let numberVerified: (typeof verify)[0] | null = null;
|
||||||
|
|
||||||
// Brazilian numbers
|
// Brazilian numbers
|
||||||
if (user.number.startsWith('55')) {
|
if (user.number.startsWith('55')) {
|
||||||
const numberWithDigit =
|
const numberWithDigit =
|
||||||
@ -3425,32 +3648,27 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// For @lid numbers, always consider them as valid
|
|
||||||
const lidVerifiedUsers: OnWhatsAppDto[] = lidUsers.map((user) => {
|
|
||||||
return new OnWhatsAppDto(
|
|
||||||
user.jid,
|
|
||||||
true,
|
|
||||||
user.number,
|
|
||||||
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
|
|
||||||
user.jid.split('@')[1],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combine results
|
// Combine results
|
||||||
onWhatsapp.push(...normalVerifiedUsers, ...lidVerifiedUsers);
|
onWhatsapp.push(...verifiedUsers);
|
||||||
|
|
||||||
// Save to cache only valid numbers
|
// TODO: Salvar no cache apenas números que NÃO estavam no cache
|
||||||
|
const numbersToCache = onWhatsapp.filter((user) => {
|
||||||
|
if (!user.exists) return false;
|
||||||
|
// Verifica se estava no cache usando jidOptions
|
||||||
|
const cached = cachedNumbers?.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
||||||
|
return !cached;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (numbersToCache.length > 0) {
|
||||||
|
this.logger.verbose(`Salvando ${numbersToCache.length} números no cache`);
|
||||||
await saveOnWhatsappCache(
|
await saveOnWhatsappCache(
|
||||||
onWhatsapp
|
numbersToCache.map((user) => ({
|
||||||
.filter((user) => user.exists)
|
|
||||||
.map((user) => ({
|
|
||||||
remoteJid: user.jid,
|
remoteJid: user.jid,
|
||||||
jidOptions: user.jid.replace('+', ''),
|
lid: user.lid === 'lid' ? 'lid' : undefined,
|
||||||
lid: user.lid,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return onWhatsapp;
|
return onWhatsapp;
|
||||||
}
|
}
|
||||||
@ -3633,11 +3851,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
|
||||||
Object.keys(msg.message).length === 1 &&
|
this.logger.verbose('Message contains only messageContextInfo, skipping media processing');
|
||||||
Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo')
|
return null;
|
||||||
) {
|
|
||||||
throw 'The message is messageContextInfo';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaMessage: any;
|
let mediaMessage: any;
|
||||||
@ -4394,24 +4610,37 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
throw new Error('Method not available in the Baileys service');
|
throw new Error('Method not available in the Baileys service');
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertLongToNumber(obj: any): any {
|
private deserializeMessageBuffers(obj: any): any {
|
||||||
if (obj === null || obj === undefined) {
|
if (obj === null || obj === undefined) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Long.isLong(obj)) {
|
if (typeof obj === 'object' && !Array.isArray(obj) && !Buffer.isBuffer(obj)) {
|
||||||
return obj.toNumber();
|
const keys = Object.keys(obj);
|
||||||
|
const isIndexedObject = keys.every((key) => !isNaN(Number(key)));
|
||||||
|
|
||||||
|
if (isIndexedObject && keys.length > 0) {
|
||||||
|
const values = keys.sort((a, b) => Number(a) - Number(b)).map((key) => obj[key]);
|
||||||
|
return new Uint8Array(values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is Buffer?, converter to Uint8Array
|
||||||
|
if (Buffer.isBuffer(obj)) {
|
||||||
|
return new Uint8Array(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process arrays recursively
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
return obj.map((item) => this.convertLongToNumber(item));
|
return obj.map((item) => this.deserializeMessageBuffers(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process objects recursively
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
const converted: any = {};
|
const converted: any = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
converted[key] = this.convertLongToNumber(obj[key]);
|
converted[key] = this.deserializeMessageBuffers(obj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return converted;
|
return converted;
|
||||||
@ -4432,8 +4661,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
? 'Você'
|
? 'Você'
|
||||||
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
||||||
status: status[message.status],
|
status: status[message.status],
|
||||||
message: this.convertLongToNumber({ ...message.message }),
|
message: this.deserializeMessageBuffers({ ...message.message }),
|
||||||
contextInfo: this.convertLongToNumber(contentMsg?.contextInfo),
|
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),
|
||||||
messageType: contentType || 'unknown',
|
messageType: contentType || 'unknown',
|
||||||
messageTimestamp: Long.isLong(message.messageTimestamp)
|
messageTimestamp: Long.isLong(message.messageTimestamp)
|
||||||
? message.messageTimestamp.toNumber()
|
? message.messageTimestamp.toNumber()
|
||||||
@ -4807,7 +5036,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
AND: [
|
AND: [
|
||||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
|
||||||
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
@ -4837,7 +5065,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
AND: [
|
AND: [
|
||||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
|
||||||
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {},
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
|
|||||||
@ -211,7 +211,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
try {
|
try {
|
||||||
if (mediaType === 'audio') {
|
if (mediaType === 'audio') {
|
||||||
await instance.audioWhatsapp({
|
await instance.audioWhatsapp({
|
||||||
number: remoteJid.split('@')[0],
|
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||||
delay: (settings as any)?.delayMessage || 1000,
|
delay: (settings as any)?.delayMessage || 1000,
|
||||||
audio: url,
|
audio: url,
|
||||||
caption: altText,
|
caption: altText,
|
||||||
@ -219,7 +219,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
} else {
|
} else {
|
||||||
await instance.mediaMessage(
|
await instance.mediaMessage(
|
||||||
{
|
{
|
||||||
number: remoteJid.split('@')[0],
|
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||||
delay: (settings as any)?.delayMessage || 1000,
|
delay: (settings as any)?.delayMessage || 1000,
|
||||||
mediatype: mediaType,
|
mediatype: mediaType,
|
||||||
media: url,
|
media: url,
|
||||||
@ -290,7 +290,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await instance.textMessage(
|
await instance.textMessage(
|
||||||
{
|
{
|
||||||
number: remoteJid.split('@')[0],
|
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||||
delay: settings?.delayMessage || 1000,
|
delay: settings?.delayMessage || 1000,
|
||||||
text: message,
|
text: message,
|
||||||
linkPreview,
|
linkPreview,
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||||
import { PrismaRepository } from '@api/repository/repository.service';
|
|
||||||
import { waMonitor } from '@api/server.module';
|
|
||||||
import { CacheService } from '@api/services/cache.service';
|
|
||||||
import { CacheEngine } from '@cache/cacheengine';
|
|
||||||
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
||||||
import { BadRequestException } from '@exceptions';
|
import { BadRequestException } from '@exceptions';
|
||||||
import { isURL } from 'class-validator';
|
import { isURL } from 'class-validator';
|
||||||
@ -13,7 +9,6 @@ export class ChatwootController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly chatwootService: ChatwootService,
|
private readonly chatwootService: ChatwootService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly prismaRepository: PrismaRepository,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
||||||
@ -84,9 +79,6 @@ export class ChatwootController {
|
|||||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||||
|
|
||||||
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
|
return this.chatwootService.receiveWebhook(instance, data);
|
||||||
const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache);
|
|
||||||
|
|
||||||
return chatwootService.receiveWebhook(instance, data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,17 +23,16 @@ import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageM
|
|||||||
import i18next from '@utils/i18n';
|
import i18next from '@utils/i18n';
|
||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { proto, WAMessageKey } from 'baileys';
|
import { WAMessageContent, WAMessageKey } from 'baileys';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import { Jimp, JimpMime } from 'jimp';
|
import { Jimp, JimpMime } from 'jimp';
|
||||||
|
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import mimeTypes from 'mime-types';
|
import mimeTypes from 'mime-types';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds
|
|
||||||
|
|
||||||
interface ChatwootMessage {
|
interface ChatwootMessage {
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
inboxId?: number;
|
inboxId?: number;
|
||||||
@ -45,22 +44,6 @@ interface ChatwootMessage {
|
|||||||
export class ChatwootService {
|
export class ChatwootService {
|
||||||
private readonly logger = new Logger('ChatwootService');
|
private readonly logger = new Logger('ChatwootService');
|
||||||
|
|
||||||
// HTTP timeout constants
|
|
||||||
private readonly MEDIA_DOWNLOAD_TIMEOUT_MS = 60000; // 60 seconds for large files
|
|
||||||
|
|
||||||
// S3/MinIO retry configuration (external storage - longer delays, fewer retries)
|
|
||||||
private readonly S3_MAX_RETRIES = 3;
|
|
||||||
private readonly S3_BASE_DELAY_MS = 1000; // Base delay: 1 second
|
|
||||||
private readonly S3_MAX_DELAY_MS = 8000; // Max delay: 8 seconds
|
|
||||||
|
|
||||||
// Database polling retry configuration (internal DB - shorter delays, more retries)
|
|
||||||
private readonly DB_POLLING_MAX_RETRIES = 5;
|
|
||||||
private readonly DB_POLLING_BASE_DELAY_MS = 100; // Base delay: 100ms
|
|
||||||
private readonly DB_POLLING_MAX_DELAY_MS = 2000; // Max delay: 2 seconds
|
|
||||||
|
|
||||||
// Webhook processing delay
|
|
||||||
private readonly WEBHOOK_INITIAL_DELAY_MS = 500; // Initial delay before processing webhook
|
|
||||||
|
|
||||||
// Lock polling delay
|
// Lock polling delay
|
||||||
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||||
|
|
||||||
@ -363,6 +346,16 @@ export class ChatwootService {
|
|||||||
|
|
||||||
return contact;
|
return contact;
|
||||||
} catch (error) {
|
} 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');
|
this.logger.error('Error creating contact');
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
@ -432,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) {
|
public async findContact(instance: InstanceDto, phoneNumber: string) {
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@ -588,8 +630,10 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createConversation(instance: InstanceDto, body: any) {
|
public async createConversation(instance: InstanceDto, body: any) {
|
||||||
const isLid = body.key.addressingMode === 'lid' && body.key.remoteJidAlt;
|
const isLid = body.key.addressingMode === 'lid';
|
||||||
const remoteJid = isLid ? body.key.remoteJidAlt : body.key.remoteJid;
|
const isGroup = body.key.remoteJid.endsWith('@g.us');
|
||||||
|
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
|
||||||
|
const { remoteJid } = body.key;
|
||||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||||
const maxWaitTime = 5000; // 5 seconds
|
const maxWaitTime = 5000; // 5 seconds
|
||||||
@ -598,19 +642,19 @@ export class ChatwootService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Processa atualização de contatos já criados @lid
|
// Processa atualização de contatos já criados @lid
|
||||||
if (isLid && body.key.remoteJidAlt !== body.key.remoteJid) {
|
if (phoneNumber && remoteJid && !isGroup) {
|
||||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||||
if (contact && contact.identifier !== body.key.remoteJidAlt) {
|
if (contact && contact.identifier !== remoteJid) {
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.remoteJidAlt: ${body.key.remoteJidAlt}`,
|
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
|
||||||
);
|
);
|
||||||
const updateContact = await this.updateContact(instance, contact.id, {
|
const updateContact = await this.updateContact(instance, contact.id, {
|
||||||
identifier: body.key.remoteJidAlt,
|
identifier: phoneNumber,
|
||||||
phone_number: `+${body.key.remoteJidAlt.split('@')[0]}`,
|
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateContact === null) {
|
if (updateContact === null) {
|
||||||
const baseContact = await this.findContact(instance, body.key.remoteJidAlt.split('@')[0]);
|
const baseContact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||||
if (baseContact) {
|
if (baseContact) {
|
||||||
await this.mergeContacts(baseContact.id, contact.id);
|
await this.mergeContacts(baseContact.id, contact.id);
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
@ -626,14 +670,16 @@ export class ChatwootService {
|
|||||||
// If it already exists in the cache, return conversationId
|
// If it already exists in the cache, return conversationId
|
||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
|
||||||
let conversationExists: conversation | boolean;
|
let conversationExists: any;
|
||||||
try {
|
try {
|
||||||
conversationExists = await client.conversations.get({
|
conversationExists = await client.conversations.get({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
conversationId: conversationId,
|
conversationId: conversationId,
|
||||||
});
|
});
|
||||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
this.logger.verbose(
|
||||||
|
`Conversation exists: ID: ${conversationExists.id} - Name: ${conversationExists.meta.sender.name} - Identifier: ${conversationExists.meta.sender.identifier}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error getting conversation: ${error}`);
|
this.logger.error(`Error getting conversation: ${error}`);
|
||||||
conversationExists = false;
|
conversationExists = false;
|
||||||
@ -677,8 +723,7 @@ export class ChatwootService {
|
|||||||
return (await this.cache.get(cacheKey)) as number;
|
return (await this.cache.get(cacheKey)) as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGroup = remoteJid.includes('@g.us');
|
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].split(':')[0];
|
||||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0].split(':')[0];
|
|
||||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||||
const filterInbox = await this.getInbox(instance);
|
const filterInbox = await this.getInbox(instance);
|
||||||
if (!filterInbox) return null;
|
if (!filterInbox) return null;
|
||||||
@ -686,19 +731,22 @@ export class ChatwootService {
|
|||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
this.logger.verbose(`Processing group conversation`);
|
this.logger.verbose(`Processing group conversation`);
|
||||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);
|
||||||
|
|
||||||
|
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
|
||||||
nameContact = `${group.subject} (GROUP)`;
|
nameContact = `${group.subject} (GROUP)`;
|
||||||
|
|
||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||||
body.key.participant.split('@')[0],
|
participantJid.split('@')[0],
|
||||||
);
|
);
|
||||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||||
|
|
||||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
|
||||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
|
||||||
|
|
||||||
if (findParticipant) {
|
if (findParticipant) {
|
||||||
|
this.logger.verbose(
|
||||||
|
`Found participant: ID:${findParticipant.id} - Name: ${findParticipant.name} - identifier: ${findParticipant.identifier}`,
|
||||||
|
);
|
||||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||||
await this.updateContact(instance, findParticipant.id, {
|
await this.updateContact(instance, findParticipant.id, {
|
||||||
name: body.pushName,
|
name: body.pushName,
|
||||||
@ -708,12 +756,12 @@ export class ChatwootService {
|
|||||||
} else {
|
} else {
|
||||||
await this.createContact(
|
await this.createContact(
|
||||||
instance,
|
instance,
|
||||||
body.key.participant.split('@')[0],
|
participantJid.split('@')[0].split(':')[0],
|
||||||
filterInbox.id,
|
filterInbox.id,
|
||||||
false,
|
false,
|
||||||
body.pushName,
|
body.pushName,
|
||||||
picture_url.profilePictureUrl || null,
|
picture_url.profilePictureUrl || null,
|
||||||
body.key.participant,
|
participantJid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -721,23 +769,17 @@ export class ChatwootService {
|
|||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||||
|
|
||||||
|
this.logger.verbose(`Searching contact for: ${chatId}`);
|
||||||
let contact = await this.findContact(instance, chatId);
|
let contact = await this.findContact(instance, chatId);
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
this.logger.verbose(`Found contact: ID:${contact.id} - Name:${contact.name}`);
|
||||||
if (!body.key.fromMe) {
|
if (!body.key.fromMe) {
|
||||||
const waProfilePictureFile =
|
const waProfilePictureFile =
|
||||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||||
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||||
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
||||||
const nameNeedsUpdate =
|
const nameNeedsUpdate = !contact.name || contact.name === chatId;
|
||||||
!contact.name ||
|
|
||||||
contact.name === chatId ||
|
|
||||||
(`+${chatId}`.startsWith('+55')
|
|
||||||
? this.getNumbers(`+${chatId}`).some(
|
|
||||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
|
||||||
)
|
|
||||||
: false);
|
|
||||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||||
@ -756,7 +798,7 @@ export class ChatwootService {
|
|||||||
isGroup,
|
isGroup,
|
||||||
nameContact,
|
nameContact,
|
||||||
picture_url.profilePictureUrl || null,
|
picture_url.profilePictureUrl || null,
|
||||||
remoteJid,
|
phoneNumber,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -772,7 +814,6 @@ export class ChatwootService {
|
|||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
id: contactId,
|
id: contactId,
|
||||||
})) as any;
|
})) as any;
|
||||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
|
||||||
|
|
||||||
if (!contactConversations || !contactConversations.payload) {
|
if (!contactConversations || !contactConversations.payload) {
|
||||||
this.logger.error(`No conversations found or payload is undefined`);
|
this.logger.error(`No conversations found or payload is undefined`);
|
||||||
@ -784,7 +825,9 @@ export class ChatwootService {
|
|||||||
);
|
);
|
||||||
if (inboxConversation) {
|
if (inboxConversation) {
|
||||||
if (this.provider.reopenConversation) {
|
if (this.provider.reopenConversation) {
|
||||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
this.logger.verbose(
|
||||||
|
`Found conversation in reopenConversation mode: ID: ${inboxConversation.id} - Name: ${inboxConversation.meta.sender.name} - Identifier: ${inboxConversation.meta.sender.identifier}`,
|
||||||
|
);
|
||||||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||||||
await client.conversations.toggleStatus({
|
await client.conversations.toggleStatus({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
@ -804,7 +847,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (inboxConversation) {
|
if (inboxConversation) {
|
||||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||||
this.cache.set(cacheKey, inboxConversation.id, 8 * 3600);
|
this.cache.set(cacheKey, inboxConversation.id, 1800);
|
||||||
return inboxConversation.id;
|
return inboxConversation.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -818,14 +861,6 @@ export class ChatwootService {
|
|||||||
data['status'] = 'pending';
|
data['status'] = 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Triple check after lock
|
|
||||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
|
||||||
*/
|
|
||||||
if (await this.cache.has(cacheKey)) {
|
|
||||||
return (await this.cache.get(cacheKey)) as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversation = await client.conversations.create({
|
const conversation = await client.conversations.create({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
data,
|
data,
|
||||||
@ -837,7 +872,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||||||
this.cache.set(cacheKey, conversation.id, 8 * 3600);
|
this.cache.set(cacheKey, conversation.id, 1800);
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
} finally {
|
} finally {
|
||||||
await this.cache.delete(lockKey);
|
await this.cache.delete(lockKey);
|
||||||
@ -1158,140 +1193,20 @@ export class ChatwootService {
|
|||||||
|
|
||||||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||||||
try {
|
try {
|
||||||
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
|
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||||
// URLs presigned podem expirar, então convertemos para base64
|
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
||||||
let mediaBuffer: Buffer;
|
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
||||||
let mimeType: string;
|
|
||||||
let fileName: string;
|
|
||||||
|
|
||||||
try {
|
if (!mimeType) {
|
||||||
this.logger.verbose(`Downloading media from: ${media}`);
|
const parts = media.split('/');
|
||||||
|
fileName = decodeURIComponent(parts[parts.length - 1]);
|
||||||
|
|
||||||
// Tentar fazer download do arquivo com autenticação do Chatwoot
|
|
||||||
// maxRedirects: 0 para não seguir redirects automaticamente
|
|
||||||
const response = await axios.get(media, {
|
const response = await axios.get(media, {
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
|
||||||
headers: {
|
|
||||||
api_access_token: this.provider.token,
|
|
||||||
},
|
|
||||||
maxRedirects: 0, // Não seguir redirects automaticamente
|
|
||||||
validateStatus: (status) => status < 500, // Aceitar redirects (301, 302, 307)
|
|
||||||
});
|
});
|
||||||
|
mimeType = response.headers['content-type'];
|
||||||
this.logger.verbose(`Initial response status: ${response.status}`);
|
|
||||||
|
|
||||||
// Se for redirect, pegar a URL de destino e fazer novo request
|
|
||||||
if (response.status >= 300 && response.status < 400) {
|
|
||||||
const redirectUrl = response.headers.location;
|
|
||||||
this.logger.verbose(`Redirect to: ${redirectUrl}`);
|
|
||||||
|
|
||||||
if (redirectUrl) {
|
|
||||||
// Fazer novo request para a URL do S3/MinIO (sem autenticação, pois é presigned URL)
|
|
||||||
// IMPORTANTE: Chatwoot pode gerar a URL presigned ANTES de fazer upload
|
|
||||||
// Vamos tentar com retry usando exponential backoff se receber 404 (arquivo ainda não disponível)
|
|
||||||
this.logger.verbose('Downloading from S3/MinIO...');
|
|
||||||
|
|
||||||
let s3Response;
|
|
||||||
let retryCount = 0;
|
|
||||||
const maxRetries = this.S3_MAX_RETRIES;
|
|
||||||
const baseDelay = this.S3_BASE_DELAY_MS;
|
|
||||||
const maxDelay = this.S3_MAX_DELAY_MS;
|
|
||||||
|
|
||||||
while (retryCount <= maxRetries) {
|
|
||||||
s3Response = await axios.get(redirectUrl, {
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
|
||||||
validateStatus: (status) => status < 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.verbose(
|
|
||||||
`S3 response status: ${s3Response.status}, size: ${s3Response.data?.byteLength || 0} bytes (attempt ${retryCount + 1}/${maxRetries + 1})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Se não for 404, sair do loop
|
|
||||||
if (s3Response.status !== 404) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se for 404 e ainda tem tentativas, aguardar com exponential backoff e tentar novamente
|
|
||||||
if (retryCount < maxRetries) {
|
|
||||||
// Exponential backoff com max delay (seguindo padrão do webhook controller)
|
|
||||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
|
||||||
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
|
||||||
this.logger.warn(
|
|
||||||
`File not yet available in S3/MinIO (attempt ${retryCount + 1}/${maxRetries + 1}). Retrying in ${backoffDelay}ms with exponential backoff...`,
|
|
||||||
);
|
|
||||||
this.logger.verbose(`MinIO Response: ${errorBody}`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
||||||
retryCount++;
|
|
||||||
} else {
|
|
||||||
// Última tentativa falhou
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Após todas as tentativas, verificar o status final
|
|
||||||
if (s3Response.status === 404) {
|
|
||||||
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
|
||||||
this.logger.error(`File not found in S3/MinIO after ${maxRetries + 1} attempts. URL: ${redirectUrl}`);
|
|
||||||
this.logger.error(`MinIO Error Response: ${errorBody}`);
|
|
||||||
throw new Error(
|
|
||||||
'File not found in S3/MinIO (404). The file may have been deleted, the URL is incorrect, or Chatwoot has not finished uploading yet.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s3Response.status === 403) {
|
|
||||||
this.logger.error(`Access denied to S3/MinIO. URL may have expired: ${redirectUrl}`);
|
|
||||||
throw new Error(
|
|
||||||
'Access denied to S3/MinIO (403). Presigned URL may have expired. Check S3_PRESIGNED_EXPIRATION setting.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s3Response.status >= 400) {
|
|
||||||
this.logger.error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
|
||||||
throw new Error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaBuffer = Buffer.from(s3Response.data);
|
|
||||||
mimeType = s3Response.headers['content-type'] || 'application/octet-stream';
|
|
||||||
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes from S3, type: ${mimeType}`);
|
|
||||||
} else {
|
|
||||||
this.logger.error('Redirect response without Location header');
|
|
||||||
throw new Error('Redirect without Location header');
|
|
||||||
}
|
|
||||||
} else if (response.status === 404) {
|
|
||||||
this.logger.error(`File not found (404) at: ${media}`);
|
|
||||||
throw new Error('File not found (404). The attachment may not exist in Chatwoot storage.');
|
|
||||||
} else if (response.status >= 400) {
|
|
||||||
this.logger.error(`HTTP ${response.status}: ${response.statusText} for URL: ${media}`);
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
} else {
|
|
||||||
// Download direto sem redirect
|
|
||||||
mediaBuffer = Buffer.from(response.data);
|
|
||||||
mimeType = response.headers['content-type'] || 'application/octet-stream';
|
|
||||||
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes directly, type: ${mimeType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrair nome do arquivo da URL ou usar o content-disposition
|
|
||||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
|
||||||
if (parsedMedia?.name && parsedMedia?.ext) {
|
|
||||||
fileName = parsedMedia.name + parsedMedia.ext;
|
|
||||||
} else {
|
|
||||||
const parts = media.split('/');
|
|
||||||
fileName = decodeURIComponent(parts[parts.length - 1].split('?')[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.verbose(`File name: ${fileName}, size: ${mediaBuffer.length} bytes`);
|
|
||||||
} catch (downloadError) {
|
|
||||||
this.logger.error('[MEDIA DOWNLOAD] ❌ Error downloading media from: ' + media);
|
|
||||||
this.logger.error(`[MEDIA DOWNLOAD] Error message: ${downloadError.message}`);
|
|
||||||
this.logger.error(`[MEDIA DOWNLOAD] Error stack: ${downloadError.stack}`);
|
|
||||||
this.logger.error(`[MEDIA DOWNLOAD] Full error: ${JSON.stringify(downloadError, null, 2)}`);
|
|
||||||
throw new Error(`Failed to download media: ${downloadError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determinar o tipo de mídia pelo mimetype
|
|
||||||
let type = 'document';
|
let type = 'document';
|
||||||
|
|
||||||
switch (mimeType.split('/')[0]) {
|
switch (mimeType.split('/')[0]) {
|
||||||
@ -1309,13 +1224,11 @@ export class ChatwootService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para áudio, usar base64 com data URI
|
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
const base64Audio = `data:${mimeType};base64,${mediaBuffer.toString('base64')}`;
|
|
||||||
const data: SendAudioDto = {
|
const data: SendAudioDto = {
|
||||||
number: number,
|
number: number,
|
||||||
audio: base64Audio,
|
audio: media,
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1326,12 +1239,8 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
|
|
||||||
const base64Media = mediaBuffer.toString('base64');
|
|
||||||
|
|
||||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||||
const parsedExt = path.parse(fileName)?.ext;
|
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||||
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
|
|
||||||
type = 'document';
|
type = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,7 +1248,7 @@ export class ChatwootService {
|
|||||||
number: number,
|
number: number,
|
||||||
mediatype: type as any,
|
mediatype: type as any,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
media: base64Media, // Base64 puro, sem prefixo
|
media: media,
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@ -1395,87 +1304,9 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Processa deleção de mensagem em background
|
|
||||||
* Método assíncrono chamado via setImmediate para não bloquear resposta do webhook
|
|
||||||
*/
|
|
||||||
private async processDeletion(instance: InstanceDto, body: any, deleteLockKey: string) {
|
|
||||||
this.logger.warn(`[DELETE] 🗑️ Processing deletion - messageId: ${body.id}`);
|
|
||||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
|
||||||
|
|
||||||
// Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos)
|
|
||||||
const messages = await this.prismaRepository.message.findMany({
|
|
||||||
where: {
|
|
||||||
chatwootMessageId: body.id,
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (messages && messages.length > 0) {
|
|
||||||
this.logger.warn(`[DELETE] Found ${messages.length} message(s) to delete from Chatwoot message ${body.id}`);
|
|
||||||
this.logger.verbose(`[DELETE] Messages keys: ${messages.map((m) => (m.key as any)?.id).join(', ')}`);
|
|
||||||
|
|
||||||
// Deletar cada mensagem no WhatsApp
|
|
||||||
for (const message of messages) {
|
|
||||||
const key = message.key as WAMessageKey;
|
|
||||||
this.logger.warn(
|
|
||||||
`[DELETE] Attempting to delete WhatsApp message - keyId: ${key?.id}, remoteJid: ${key?.remoteJid}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
|
||||||
this.logger.warn(`[DELETE] ✅ Message ${key.id} deleted in WhatsApp successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`[DELETE] ❌ Error deleting message ${key.id} in WhatsApp: ${error}`);
|
|
||||||
this.logger.error(`[DELETE] Error details: ${JSON.stringify(error, null, 2)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remover todas as mensagens do banco de dados
|
|
||||||
await this.prismaRepository.message.deleteMany({
|
|
||||||
where: {
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
chatwootMessageId: body.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.logger.warn(`[DELETE] ✅ SUCCESS: ${messages.length} message(s) deleted from WhatsApp and database`);
|
|
||||||
} else {
|
|
||||||
// Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
|
|
||||||
this.logger.warn(`[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${body.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Liberar lock após processar
|
|
||||||
await this.cache.delete(deleteLockKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async receiveWebhook(instance: InstanceDto, body: any) {
|
public async receiveWebhook(instance: InstanceDto, body: any) {
|
||||||
try {
|
try {
|
||||||
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
// para evitar race condition com webhooks duplicados
|
|
||||||
let isDeletionEvent = false;
|
|
||||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
|
||||||
isDeletionEvent = true;
|
|
||||||
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
|
||||||
|
|
||||||
// Verificar se já está processando esta deleção
|
|
||||||
if (await this.cache.has(deleteLockKey)) {
|
|
||||||
this.logger.warn(`[DELETE] ⏭️ SKIPPING: Deletion already in progress for messageId: ${body.id}`);
|
|
||||||
return { message: 'already_processing' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adquirir lock IMEDIATAMENTE por 30 segundos
|
|
||||||
await this.cache.set(deleteLockKey, true, 30);
|
|
||||||
|
|
||||||
this.logger.warn(
|
|
||||||
`[WEBHOOK-DELETE] Event: ${body.event}, messageId: ${body.id}, conversation: ${body.conversation?.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Para deleções, processar IMEDIATAMENTE (sem delay)
|
|
||||||
// Para outros eventos, aguardar delay inicial
|
|
||||||
if (!isDeletionEvent) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, this.WEBHOOK_INITIAL_DELAY_MS));
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@ -1494,39 +1325,6 @@ export class ChatwootService {
|
|||||||
this.cache.delete(keyToDelete);
|
this.cache.delete(keyToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log para debug de mensagens deletadas
|
|
||||||
if (body.event === 'message_updated') {
|
|
||||||
this.logger.verbose(
|
|
||||||
`Message updated event - deleted: ${body.content_attributes?.deleted}, messageId: ${body.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processar deleção de mensagem ANTES das outras validações
|
|
||||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
|
||||||
// Lock já foi adquirido no início do método (antes do delay)
|
|
||||||
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
|
||||||
|
|
||||||
// ESTRATÉGIA: Processar em background e responder IMEDIATAMENTE
|
|
||||||
// Isso evita timeout do Chatwoot (5s) quando há muitas imagens (> 5s de processamento)
|
|
||||||
this.logger.warn(`[DELETE] 🚀 Starting background deletion - messageId: ${body.id}`);
|
|
||||||
|
|
||||||
// Executar em background (sem await) - não bloqueia resposta do webhook
|
|
||||||
setImmediate(async () => {
|
|
||||||
try {
|
|
||||||
await this.processDeletion(instance, body, deleteLockKey);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`[DELETE] ❌ Background deletion failed for messageId ${body.id}: ${error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// RESPONDER IMEDIATAMENTE ao Chatwoot (< 50ms)
|
|
||||||
return {
|
|
||||||
message: 'deletion_accepted',
|
|
||||||
messageId: body.id,
|
|
||||||
note: 'Deletion is being processed in background',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!body?.conversation ||
|
!body?.conversation ||
|
||||||
body.private ||
|
body.private ||
|
||||||
@ -1548,6 +1346,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
|
instance.instanceId = waInstance.instanceId;
|
||||||
|
|
||||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||||
const message = await this.prismaRepository.message.findFirst({
|
const message = await this.prismaRepository.message.findFirst({
|
||||||
@ -1643,10 +1442,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
|
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
|
||||||
if (
|
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
|
||||||
body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' &&
|
|
||||||
body?.conversation?.messages[0]?.id === body?.id
|
|
||||||
) {
|
|
||||||
return { message: 'bot' };
|
return { message: 'bot' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1670,8 +1466,6 @@ export class ChatwootService {
|
|||||||
|
|
||||||
for (const message of body.conversation.messages) {
|
for (const message of body.conversation.messages) {
|
||||||
if (message.attachments && message.attachments.length > 0) {
|
if (message.attachments && message.attachments.length > 0) {
|
||||||
// Processa anexos de forma assíncrona para não bloquear o webhook
|
|
||||||
const processAttachments = async () => {
|
|
||||||
for (const attachment of message.attachments) {
|
for (const attachment of message.attachments) {
|
||||||
if (!messageReceived) {
|
if (!messageReceived) {
|
||||||
formatText = null;
|
formatText = null;
|
||||||
@ -1681,7 +1475,6 @@ export class ChatwootService {
|
|||||||
quoted: await this.getQuotedMessage(body, instance),
|
quoted: await this.getQuotedMessage(body, instance),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
const messageSent = await this.sendAttachment(
|
const messageSent = await this.sendAttachment(
|
||||||
waInstance,
|
waInstance,
|
||||||
chatId,
|
chatId,
|
||||||
@ -1689,16 +1482,13 @@ export class ChatwootService {
|
|||||||
formatText,
|
formatText,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!messageSent && body.conversation?.id) {
|
if (!messageSent && body.conversation?.id) {
|
||||||
this.onSendMessageError(instance, body.conversation?.id);
|
this.onSendMessageError(instance, body.conversation?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageSent) {
|
|
||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
{
|
{
|
||||||
...messageSent,
|
...messageSent,
|
||||||
owner: instance.instanceName,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
@ -1709,24 +1499,11 @@ export class ChatwootService {
|
|||||||
instance,
|
instance,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
if (body.conversation?.id) {
|
|
||||||
this.onSendMessageError(instance, body.conversation?.id, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Executa em background sem bloquear
|
|
||||||
processAttachments().catch((error) => {
|
|
||||||
this.logger.error(error);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const data: SendTextDto = {
|
const data: SendTextDto = {
|
||||||
number: chatId,
|
number: chatId,
|
||||||
text: formatText,
|
text: formatText,
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
quoted: await this.getQuotedMessage(body, instance),
|
quoted: await this.getQuotedMessage(body, instance),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1744,7 +1521,9 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
messageSent, // Já tem instanceId
|
{
|
||||||
|
...messageSent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
inboxId: body.inbox?.id,
|
inboxId: body.inbox?.id,
|
||||||
@ -1811,7 +1590,7 @@ export class ChatwootService {
|
|||||||
const data: SendTextDto = {
|
const data: SendTextDto = {
|
||||||
number: chatId,
|
number: chatId,
|
||||||
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
@ -1835,55 +1614,6 @@ export class ChatwootService {
|
|||||||
const key = message.key as WAMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
if (!chatwootMessageIds.messageId || !key?.id) {
|
||||||
this.logger.verbose(
|
|
||||||
`Skipping updateChatwootMessageId - messageId: ${chatwootMessageIds.messageId}, keyId: ${key?.id}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use instanceId from message or fallback to instance
|
|
||||||
const instanceId = message.instanceId || instance.instanceId;
|
|
||||||
|
|
||||||
this.logger.verbose(
|
|
||||||
`Updating message with chatwootMessageId: ${chatwootMessageIds.messageId}, keyId: ${key.id}, instanceId: ${instanceId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verifica se a mensagem existe antes de atualizar usando polling com exponential backoff
|
|
||||||
let retries = 0;
|
|
||||||
const maxRetries = this.DB_POLLING_MAX_RETRIES;
|
|
||||||
const baseDelay = this.DB_POLLING_BASE_DELAY_MS;
|
|
||||||
const maxDelay = this.DB_POLLING_MAX_DELAY_MS;
|
|
||||||
let messageExists = false;
|
|
||||||
|
|
||||||
while (retries < maxRetries && !messageExists) {
|
|
||||||
const existingMessage = await this.prismaRepository.message.findFirst({
|
|
||||||
where: {
|
|
||||||
instanceId: instanceId,
|
|
||||||
key: {
|
|
||||||
path: ['id'],
|
|
||||||
equals: key.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingMessage) {
|
|
||||||
messageExists = true;
|
|
||||||
this.logger.verbose(`Message found in database after ${retries} retries`);
|
|
||||||
} else {
|
|
||||||
retries++;
|
|
||||||
if (retries < maxRetries) {
|
|
||||||
// Exponential backoff com max delay (seguindo padrão do sistema)
|
|
||||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retries - 1), maxDelay);
|
|
||||||
this.logger.verbose(`Message not found, retry ${retries}/${maxRetries} in ${backoffDelay}ms`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
||||||
} else {
|
|
||||||
this.logger.verbose(`Message not found after ${retries} attempts`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!messageExists) {
|
|
||||||
this.logger.warn(`Message not found in database after ${maxRetries} retries, keyId: ${key.id}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1896,14 +1626,18 @@ export class ChatwootService {
|
|||||||
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
||||||
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
||||||
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
||||||
WHERE "instanceId" = ${instanceId}
|
WHERE "instanceId" = ${instance.instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.logger.verbose(`Update result: ${result} rows affected`);
|
this.logger.verbose(`Update result: ${result} rows affected`);
|
||||||
|
|
||||||
if (this.isImportHistoryAvailable()) {
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1952,11 +1686,12 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const key = message?.key as WAMessageKey;
|
const key = message?.key as WAMessageKey;
|
||||||
|
const messageContent = message?.message as WAMessageContent;
|
||||||
|
|
||||||
if (message && key?.id) {
|
if (messageContent && key?.id) {
|
||||||
return {
|
return {
|
||||||
key: message.key as proto.IMessageKey,
|
key: key,
|
||||||
message: message.message as proto.IMessage,
|
message: messageContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1982,6 +1717,10 @@ export class ChatwootService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isInteractiveButtonMessage(messageType: string, message: any) {
|
||||||
|
return messageType === 'interactiveMessage' && message.interactiveMessage?.nativeFlowMessage?.buttons?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private getAdsMessage(msg: any) {
|
private getAdsMessage(msg: any) {
|
||||||
interface AdsMessage {
|
interface AdsMessage {
|
||||||
title: string;
|
title: string;
|
||||||
@ -2300,8 +2039,9 @@ export class ChatwootService {
|
|||||||
const adsMessage = this.getAdsMessage(body);
|
const adsMessage = this.getAdsMessage(body);
|
||||||
|
|
||||||
const reactionMessage = this.getReactionMessage(body.message);
|
const reactionMessage = this.getReactionMessage(body.message);
|
||||||
|
const isInteractiveButtonMessage = this.isInteractiveButtonMessage(body.messageType, body.message);
|
||||||
|
|
||||||
if (!bodyMessage && !isMedia && !reactionMessage) {
|
if (!bodyMessage && !isMedia && !reactionMessage && !isInteractiveButtonMessage) {
|
||||||
this.logger.warn('no body message found');
|
this.logger.warn('no body message found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2346,23 +2086,20 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (body.key.remoteJid.includes('@g.us')) {
|
if (body.key.remoteJid.includes('@g.us')) {
|
||||||
const participantName = body.pushName;
|
const participantName = body.pushName;
|
||||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
const rawPhoneNumber =
|
||||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
|
||||||
|
? body.key.participantAlt.split('@')[0].split(':')[0]
|
||||||
let formattedPhoneNumber: string;
|
: body.key.participant.split('@')[0].split(':')[0];
|
||||||
|
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
|
||||||
if (phoneMatch) {
|
|
||||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
|
||||||
} else {
|
|
||||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (!body.key.fromMe) {
|
if (!body.key.fromMe) {
|
||||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
content = bodyMessage
|
||||||
|
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
|
||||||
|
: `**${formattedPhoneNumber} - ${participantName}:**`;
|
||||||
} else {
|
} else {
|
||||||
content = `${bodyMessage}`;
|
content = bodyMessage || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = await this.sendData(
|
const send = await this.sendData(
|
||||||
@ -2429,6 +2166,50 @@ export class ChatwootService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInteractiveButtonMessage) {
|
||||||
|
const buttons = body.message.interactiveMessage.nativeFlowMessage.buttons;
|
||||||
|
this.logger.info('is Interactive Button Message: ' + JSON.stringify(buttons));
|
||||||
|
|
||||||
|
for (const button of buttons) {
|
||||||
|
const buttonParams = JSON.parse(button.buttonParamsJson);
|
||||||
|
const paymentSettings = buttonParams.payment_settings;
|
||||||
|
|
||||||
|
if (button.name === 'payment_info' && paymentSettings[0].type === 'pix_static_code') {
|
||||||
|
const pixSettings = paymentSettings[0].pix_static_code;
|
||||||
|
const pixKeyType = (() => {
|
||||||
|
switch (pixSettings.key_type) {
|
||||||
|
case 'EVP':
|
||||||
|
return 'Chave Aleatória';
|
||||||
|
case 'EMAIL':
|
||||||
|
return 'E-mail';
|
||||||
|
case 'PHONE':
|
||||||
|
return 'Telefone';
|
||||||
|
default:
|
||||||
|
return pixSettings.key_type;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const pixKey = pixSettings.key_type === 'PHONE' ? pixSettings.key.replace('+55', '') : pixSettings.key;
|
||||||
|
const content = `*${pixSettings.merchant_name}*\nChave PIX: ${pixKey} (${pixKeyType})`;
|
||||||
|
|
||||||
|
const send = await this.createMessage(
|
||||||
|
instance,
|
||||||
|
getConversation,
|
||||||
|
content,
|
||||||
|
messageType,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
body,
|
||||||
|
'WAID:' + body.key.id,
|
||||||
|
quotedMsg,
|
||||||
|
);
|
||||||
|
if (!send) this.logger.warn('message not sent');
|
||||||
|
} else {
|
||||||
|
this.logger.warn('Interactive Button Message not mapped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl;
|
const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl;
|
||||||
if (isAdsMessage) {
|
if (isAdsMessage) {
|
||||||
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
|
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
|
||||||
@ -2487,16 +2268,11 @@ export class ChatwootService {
|
|||||||
|
|
||||||
if (body.key.remoteJid.includes('@g.us')) {
|
if (body.key.remoteJid.includes('@g.us')) {
|
||||||
const participantName = body.pushName;
|
const participantName = body.pushName;
|
||||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
const rawPhoneNumber =
|
||||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
|
||||||
|
? body.key.participantAlt.split('@')[0].split(':')[0]
|
||||||
let formattedPhoneNumber: string;
|
: body.key.participant.split('@')[0].split(':')[0];
|
||||||
|
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
|
||||||
if (phoneMatch) {
|
|
||||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
|
||||||
} else {
|
|
||||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
@ -2578,8 +2354,21 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||||
const editedMessageContent =
|
const editedMessageContentRaw =
|
||||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
|
body?.editedMessage?.conversation ??
|
||||||
|
body?.editedMessage?.extendedTextMessage?.text ??
|
||||||
|
body?.editedMessage?.imageMessage?.caption ??
|
||||||
|
body?.editedMessage?.videoMessage?.caption ??
|
||||||
|
body?.editedMessage?.documentMessage?.caption ??
|
||||||
|
(typeof body?.text === 'string' ? body.text : undefined);
|
||||||
|
|
||||||
|
const editedMessageContent = (editedMessageContentRaw ?? '').trim();
|
||||||
|
|
||||||
|
if (!editedMessageContent) {
|
||||||
|
this.logger.info('[CW.EDIT] Conteúdo vazio — ignorando (DELETE tratará se for revoke).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@ -2646,7 +2435,7 @@ export class ChatwootService {
|
|||||||
const url =
|
const url =
|
||||||
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
|
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
|
||||||
`/conversations/${conversationId}/update_last_seen`;
|
`/conversations/${conversationId}/update_last_seen`;
|
||||||
chatwootRequest(this.getClientCwConfig(), {
|
await chatwootRequest(this.getClientCwConfig(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
});
|
});
|
||||||
@ -2688,7 +2477,7 @@ export class ChatwootService {
|
|||||||
chatwootImport.clearAll(instance);
|
chatwootImport.clearAll(instance);
|
||||||
}
|
}
|
||||||
// Se não foi via QR code, verifica o throttling.
|
// Se não foi via QR code, verifica o throttling.
|
||||||
else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) {
|
else if (timeSinceLastNotification >= 30000) {
|
||||||
const msgConnection = i18next.t('cw.inbox.connected');
|
const msgConnection = i18next.t('cw.inbox.connected');
|
||||||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||||||
waInstance.lastConnectionNotification = now;
|
waInstance.lastConnectionNotification = now;
|
||||||
@ -2738,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];
|
return remoteJid.replace(/:\d+/, '').split('@')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,7 +137,7 @@ class ChatwootImport {
|
|||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
phone_number = EXCLUDED.phone_number,
|
phone_number = EXCLUDED.phone_number,
|
||||||
identifier = EXCLUDED.identifier`;
|
updated_at = NOW()`;
|
||||||
|
|
||||||
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
|||||||
pushName: pushName,
|
pushName: pushName,
|
||||||
keyId: msg?.key?.id,
|
keyId: msg?.key?.id,
|
||||||
fromMe: msg?.key?.fromMe,
|
fromMe: msg?.key?.fromMe,
|
||||||
|
quotedMessage: msg?.contextInfo?.quotedMessage,
|
||||||
instanceName: instance.instanceName,
|
instanceName: instance.instanceName,
|
||||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||||
apiKey: instance.token,
|
apiKey: instance.token,
|
||||||
|
|||||||
@ -327,7 +327,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
if (message.type === 'image') {
|
if (message.type === 'image') {
|
||||||
await instance.mediaMessage(
|
await instance.mediaMessage(
|
||||||
{
|
{
|
||||||
number: session.remoteJid.split('@')[0],
|
number: session.remoteJid,
|
||||||
delay: settings?.delayMessage || 1000,
|
delay: settings?.delayMessage || 1000,
|
||||||
mediatype: 'image',
|
mediatype: 'image',
|
||||||
media: message.content.url,
|
media: message.content.url,
|
||||||
@ -342,7 +342,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
if (message.type === 'video') {
|
if (message.type === 'video') {
|
||||||
await instance.mediaMessage(
|
await instance.mediaMessage(
|
||||||
{
|
{
|
||||||
number: session.remoteJid.split('@')[0],
|
number: session.remoteJid,
|
||||||
delay: settings?.delayMessage || 1000,
|
delay: settings?.delayMessage || 1000,
|
||||||
mediatype: 'video',
|
mediatype: 'video',
|
||||||
media: message.content.url,
|
media: message.content.url,
|
||||||
@ -357,7 +357,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
if (message.type === 'audio') {
|
if (message.type === 'audio') {
|
||||||
await instance.audioWhatsapp(
|
await instance.audioWhatsapp(
|
||||||
{
|
{
|
||||||
number: session.remoteJid.split('@')[0],
|
number: session.remoteJid,
|
||||||
delay: settings?.delayMessage || 1000,
|
delay: settings?.delayMessage || 1000,
|
||||||
encoding: true,
|
encoding: true,
|
||||||
audio: message.content.url,
|
audio: message.content.url,
|
||||||
@ -441,7 +441,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
*/
|
*/
|
||||||
private async processListMessage(instance: any, formattedText: string, remoteJid: string) {
|
private async processListMessage(instance: any, formattedText: string, remoteJid: string) {
|
||||||
const listJson = {
|
const listJson = {
|
||||||
number: remoteJid.split('@')[0],
|
number: remoteJid,
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
buttonText: '',
|
buttonText: '',
|
||||||
@ -490,7 +490,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
*/
|
*/
|
||||||
private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) {
|
private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) {
|
||||||
const buttonJson = {
|
const buttonJson = {
|
||||||
number: remoteJid.split('@')[0],
|
number: remoteJid,
|
||||||
thumbnailUrl: undefined,
|
thumbnailUrl: undefined,
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|||||||
@ -14,12 +14,24 @@ export type EmitData = {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
local?: boolean;
|
local?: boolean;
|
||||||
integration?: string[];
|
integration?: string[];
|
||||||
|
extra?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EventControllerInterface {
|
export interface EventControllerInterface {
|
||||||
set(instanceName: string, data: any): Promise<any>;
|
set(instanceName: string, data: any): Promise<any>;
|
||||||
get(instanceName: string): 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 {
|
export class EventController {
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export class EventManager {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
local?: boolean;
|
local?: boolean;
|
||||||
integration?: string[];
|
integration?: string[];
|
||||||
|
extra?: Record<string, any>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await this.websocket.emit(eventData);
|
await this.websocket.emit(eventData);
|
||||||
await this.rabbitmq.emit(eventData);
|
await this.rabbitmq.emit(eventData);
|
||||||
|
|||||||
@ -262,6 +262,7 @@ export class KafkaController extends EventController implements EventControllerI
|
|||||||
sender,
|
sender,
|
||||||
apiKey,
|
apiKey,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('kafka')) {
|
if (integration && !integration.includes('kafka')) {
|
||||||
return;
|
return;
|
||||||
@ -284,6 +285,7 @@ export class KafkaController extends EventController implements EventControllerI
|
|||||||
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export class NatsController extends EventController implements EventControllerIn
|
|||||||
sender,
|
sender,
|
||||||
apiKey,
|
apiKey,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('nats')) {
|
if (integration && !integration.includes('nats')) {
|
||||||
return;
|
return;
|
||||||
@ -65,6 +66,7 @@ export class NatsController extends EventController implements EventControllerIn
|
|||||||
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -121,6 +121,7 @@ export class PusherController extends EventController implements EventController
|
|||||||
apiKey,
|
apiKey,
|
||||||
local,
|
local,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('pusher')) {
|
if (integration && !integration.includes('pusher')) {
|
||||||
return;
|
return;
|
||||||
@ -133,6 +134,7 @@ export class PusherController extends EventController implements EventController
|
|||||||
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||||
const eventName = event.replace(/_/g, '.').toLowerCase();
|
const eventName = event.replace(/_/g, '.').toLowerCase();
|
||||||
const pusherData = {
|
const pusherData = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -209,6 +209,7 @@ export class RabbitmqController extends EventController implements EventControll
|
|||||||
sender,
|
sender,
|
||||||
apiKey,
|
apiKey,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('rabbitmq')) {
|
if (integration && !integration.includes('rabbitmq')) {
|
||||||
return;
|
return;
|
||||||
@ -233,6 +234,7 @@ export class RabbitmqController extends EventController implements EventControll
|
|||||||
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -93,6 +93,7 @@ export class SqsController extends EventController implements EventControllerInt
|
|||||||
sender,
|
sender,
|
||||||
apiKey,
|
apiKey,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('sqs')) {
|
if (integration && !integration.includes('sqs')) {
|
||||||
return;
|
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 sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`;
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
apiKey,
|
apiKey,
|
||||||
local,
|
local,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('webhook')) {
|
if (integration && !integration.includes('webhook')) {
|
||||||
return;
|
return;
|
||||||
@ -90,6 +91,7 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
const regex = /^(https?:\/\/)/;
|
const regex = /^(https?:\/\/)/;
|
||||||
|
|
||||||
const webhookData = {
|
const webhookData = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -33,7 +33,10 @@ export class WebsocketController extends EventController implements EventControl
|
|||||||
const { remoteAddress } = req.socket;
|
const { remoteAddress } = req.socket;
|
||||||
const websocketConfig = configService.get<Websocket>('WEBSOCKET');
|
const websocketConfig = configService.get<Websocket>('WEBSOCKET');
|
||||||
const allowedHosts = websocketConfig.ALLOWED_HOSTS || '127.0.0.1,::1,::ffff:127.0.0.1';
|
const allowedHosts = websocketConfig.ALLOWED_HOSTS || '127.0.0.1,::1,::ffff:127.0.0.1';
|
||||||
const isAllowedHost = allowedHosts
|
const allowAllHosts = allowedHosts.trim() === '*';
|
||||||
|
const isAllowedHost =
|
||||||
|
allowAllHosts ||
|
||||||
|
allowedHosts
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((h) => h.trim())
|
.map((h) => h.trim())
|
||||||
.includes(remoteAddress);
|
.includes(remoteAddress);
|
||||||
@ -115,6 +118,7 @@ export class WebsocketController extends EventController implements EventControl
|
|||||||
sender,
|
sender,
|
||||||
apiKey,
|
apiKey,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
}: EmitData): Promise<void> {
|
}: EmitData): Promise<void> {
|
||||||
if (integration && !integration.includes('websocket')) {
|
if (integration && !integration.includes('websocket')) {
|
||||||
return;
|
return;
|
||||||
@ -127,6 +131,7 @@ export class WebsocketController extends EventController implements EventControl
|
|||||||
const configEv = event.replace(/[.-]/gm, '_').toUpperCase();
|
const configEv = event.replace(/[.-]/gm, '_').toUpperCase();
|
||||||
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET');
|
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET');
|
||||||
const message = {
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
event,
|
event,
|
||||||
instance: instanceName,
|
instance: instanceName,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@ -48,9 +48,14 @@ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|||||||
const metricsIPWhitelist = (req: Request, res: Response, next: NextFunction) => {
|
const metricsIPWhitelist = (req: Request, res: Response, next: NextFunction) => {
|
||||||
const metricsConfig = configService.get('METRICS');
|
const metricsConfig = configService.get('METRICS');
|
||||||
const allowedIPs = metricsConfig.ALLOWED_IPS?.split(',').map((ip) => ip.trim()) || ['127.0.0.1'];
|
const allowedIPs = metricsConfig.ALLOWED_IPS?.split(',').map((ip) => ip.trim()) || ['127.0.0.1'];
|
||||||
const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;
|
const clientIPs = [
|
||||||
|
req.ip,
|
||||||
|
req.connection.remoteAddress,
|
||||||
|
req.socket.remoteAddress,
|
||||||
|
req.headers['x-forwarded-for'],
|
||||||
|
].filter((ip) => ip !== undefined);
|
||||||
|
|
||||||
if (!allowedIPs.includes(clientIP)) {
|
if (allowedIPs.filter((ip) => clientIPs.includes(ip)) === 0) {
|
||||||
return res.status(403).send('Forbidden: IP not allowed');
|
return res.status(403).send('Forbidden: IP not allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { TemplateDto } from '@api/dto/template.dto';
|
import { TemplateDeleteDto, TemplateDto, TemplateEditDto } from '@api/dto/template.dto';
|
||||||
import { templateController } from '@api/server.module';
|
import { templateController } from '@api/server.module';
|
||||||
import { ConfigService } from '@config/env.config';
|
import { ConfigService } from '@config/env.config';
|
||||||
import { createMetaErrorResponse } from '@utils/errorResponse';
|
import { createMetaErrorResponse } from '@utils/errorResponse';
|
||||||
|
import { templateDeleteSchema } from '@validate/templateDelete.schema';
|
||||||
|
import { templateEditSchema } from '@validate/templateEdit.schema';
|
||||||
import { instanceSchema, templateSchema } from '@validate/validate.schema';
|
import { instanceSchema, templateSchema } from '@validate/validate.schema';
|
||||||
import { RequestHandler, Router } from 'express';
|
import { RequestHandler, Router } from 'express';
|
||||||
|
|
||||||
@ -35,6 +37,38 @@ export class TemplateRouter extends RouterBroker {
|
|||||||
res.status(errorResponse.status).json(errorResponse);
|
res.status(errorResponse.status).json(errorResponse);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.post(this.routerPath('edit'), ...guards, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await this.dataValidate<TemplateEditDto>({
|
||||||
|
request: req,
|
||||||
|
schema: templateEditSchema,
|
||||||
|
ClassRef: TemplateEditDto,
|
||||||
|
execute: (instance, data) => templateController.editTemplate(instance, data),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(HttpStatus.OK).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Template edit error:', error);
|
||||||
|
const errorResponse = createMetaErrorResponse(error, 'template_edit');
|
||||||
|
res.status(errorResponse.status).json(errorResponse);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.delete(this.routerPath('delete'), ...guards, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await this.dataValidate<TemplateDeleteDto>({
|
||||||
|
request: req,
|
||||||
|
schema: templateDeleteSchema,
|
||||||
|
ClassRef: TemplateDeleteDto,
|
||||||
|
execute: (instance, data) => templateController.deleteTemplate(instance, data),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(HttpStatus.OK).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Template delete error:', error);
|
||||||
|
const errorResponse = createMetaErrorResponse(error, 'template_delete');
|
||||||
|
res.status(errorResponse.status).json(errorResponse);
|
||||||
|
}
|
||||||
|
})
|
||||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.dataValidate<InstanceDto>({
|
const response = await this.dataValidate<InstanceDto>({
|
||||||
|
|||||||
@ -82,7 +82,7 @@ const proxyService = new ProxyService(waMonitor);
|
|||||||
export const proxyController = new ProxyController(proxyService, waMonitor);
|
export const proxyController = new ProxyController(proxyService, waMonitor);
|
||||||
|
|
||||||
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
|
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
|
||||||
export const chatwootController = new ChatwootController(chatwootService, configService, prismaRepository);
|
export const chatwootController = new ChatwootController(chatwootService, configService);
|
||||||
|
|
||||||
const settingsService = new SettingsService(waMonitor);
|
const settingsService = new SettingsService(waMonitor);
|
||||||
export const settingsController = new SettingsController(settingsService);
|
export const settingsController = new SettingsController(settingsService);
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export class ChannelStartupService {
|
|||||||
this.instance.number = instance.number;
|
this.instance.number = instance.number;
|
||||||
this.instance.token = instance.token;
|
this.instance.token = instance.token;
|
||||||
this.instance.businessId = instance.businessId;
|
this.instance.businessId = instance.businessId;
|
||||||
|
this.instance.ownerJid = instance.ownerJid;
|
||||||
|
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||||
this.chatwootService.eventWhatsapp(
|
this.chatwootService.eventWhatsapp(
|
||||||
@ -431,7 +432,13 @@ export class ChannelStartupService {
|
|||||||
return data;
|
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 serverUrl = this.configService.get<HttpServer>('SERVER').URL;
|
||||||
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
|
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
|
||||||
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
|
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
|
||||||
@ -452,6 +459,7 @@ export class ChannelStartupService {
|
|||||||
apiKey: expose && instanceApikey ? instanceApikey : null,
|
apiKey: expose && instanceApikey ? instanceApikey : null,
|
||||||
local,
|
local,
|
||||||
integration,
|
integration,
|
||||||
|
extra,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,20 +498,23 @@ export class ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async fetchContacts(query: Query<Contact>) {
|
public async fetchContacts(query: Query<Contact>) {
|
||||||
const remoteJid = query?.where?.remoteJid
|
const where: any = {
|
||||||
? query?.where?.remoteJid.includes('@')
|
|
||||||
? query.where?.remoteJid
|
|
||||||
: createJid(query.where?.remoteJid)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const where = {
|
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (remoteJid) {
|
if (query?.where?.remoteJid) {
|
||||||
|
const remoteJid = query.where.remoteJid.includes('@') ? query.where.remoteJid : createJid(query.where.remoteJid);
|
||||||
where['remoteJid'] = remoteJid;
|
where['remoteJid'] = remoteJid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query?.where?.id) {
|
||||||
|
where['id'] = query.where.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query?.where?.pushName) {
|
||||||
|
where['pushName'] = query.where.pushName;
|
||||||
|
}
|
||||||
|
|
||||||
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
||||||
where,
|
where,
|
||||||
};
|
};
|
||||||
@ -532,14 +543,12 @@ export class ChannelStartupService {
|
|||||||
|
|
||||||
public cleanMessageData(message: any) {
|
public cleanMessageData(message: any) {
|
||||||
if (!message) return message;
|
if (!message) return message;
|
||||||
|
|
||||||
const cleanedMessage = { ...message };
|
const cleanedMessage = { ...message };
|
||||||
|
|
||||||
const mediaUrl = cleanedMessage.message.mediaUrl;
|
if (cleanedMessage.message) {
|
||||||
|
const { mediaUrl } = cleanedMessage.message;
|
||||||
delete cleanedMessage.message.base64;
|
delete cleanedMessage.message.base64;
|
||||||
|
|
||||||
if (cleanedMessage.message) {
|
|
||||||
// Limpa imageMessage
|
// Limpa imageMessage
|
||||||
if (cleanedMessage.message.imageMessage) {
|
if (cleanedMessage.message.imageMessage) {
|
||||||
cleanedMessage.message.imageMessage = {
|
cleanedMessage.message.imageMessage = {
|
||||||
@ -581,9 +590,9 @@ export class ChannelStartupService {
|
|||||||
name: cleanedMessage.message.documentWithCaptionMessage.name,
|
name: cleanedMessage.message.documentWithCaptionMessage.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
|
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
|
||||||
|
}
|
||||||
|
|
||||||
return cleanedMessage;
|
return cleanedMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,14 +38,22 @@ export class WAMonitoringService {
|
|||||||
|
|
||||||
private readonly logger = new Logger('WAMonitoringService');
|
private readonly logger = new Logger('WAMonitoringService');
|
||||||
public readonly waInstances: Record<string, any> = {};
|
public readonly waInstances: Record<string, any> = {};
|
||||||
|
private readonly delInstanceTimeouts: Record<string, NodeJS.Timeout> = {};
|
||||||
|
|
||||||
private readonly providerSession: ProviderSession;
|
private readonly providerSession: ProviderSession;
|
||||||
|
|
||||||
public delInstanceTime(instance: string) {
|
public delInstanceTime(instance: string) {
|
||||||
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
|
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
|
||||||
if (typeof time === 'number' && time > 0) {
|
if (typeof time === 'number' && time > 0) {
|
||||||
setTimeout(
|
// Clear previous timeout if exists
|
||||||
|
if (this.delInstanceTimeouts[instance]) {
|
||||||
|
clearTimeout(this.delInstanceTimeouts[instance]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout and store reference
|
||||||
|
this.delInstanceTimeouts[instance] = setTimeout(
|
||||||
async () => {
|
async () => {
|
||||||
|
try {
|
||||||
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
|
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
|
||||||
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
|
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
|
||||||
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
|
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -58,12 +66,23 @@ export class WAMonitoringService {
|
|||||||
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
this.eventEmitter.emit('remove.instance', instance, 'inner');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up timeout reference
|
||||||
|
delete this.delInstanceTimeouts[instance];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
1000 * 60 * time,
|
1000 * 60 * time,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearDelInstanceTime(instance: string) {
|
||||||
|
if (this.delInstanceTimeouts[instance]) {
|
||||||
|
clearTimeout(this.delInstanceTimeouts[instance]);
|
||||||
|
delete this.delInstanceTimeouts[instance];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async instanceInfo(instanceNames?: string[]): Promise<any> {
|
public async instanceInfo(instanceNames?: string[]): Promise<any> {
|
||||||
if (instanceNames && instanceNames.length > 0) {
|
if (instanceNames && instanceNames.length > 0) {
|
||||||
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
|
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
|
||||||
@ -271,9 +290,19 @@ export class WAMonitoringService {
|
|||||||
token: instanceData.token,
|
token: instanceData.token,
|
||||||
number: instanceData.number,
|
number: instanceData.number,
|
||||||
businessId: instanceData.businessId,
|
businessId: instanceData.businessId,
|
||||||
|
ownerJid: instanceData.ownerJid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (instanceData.connectionStatus === 'open' || instanceData.connectionStatus === 'connecting') {
|
||||||
|
this.logger.info(
|
||||||
|
`Auto-connecting instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus})`,
|
||||||
|
);
|
||||||
await instance.connectToWhatsapp();
|
await instance.connectToWhatsapp();
|
||||||
|
} else {
|
||||||
|
this.logger.info(
|
||||||
|
`Skipping auto-connect for instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus || 'close'})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.waInstances[instanceData.instanceName] = instance;
|
this.waInstances[instanceData.instanceName] = instance;
|
||||||
}
|
}
|
||||||
@ -299,6 +328,7 @@ export class WAMonitoringService {
|
|||||||
token: instanceData.token,
|
token: instanceData.token,
|
||||||
number: instanceData.number,
|
number: instanceData.number,
|
||||||
businessId: instanceData.businessId,
|
businessId: instanceData.businessId,
|
||||||
|
connectionStatus: instanceData.connectionStatus as any, // Pass connection status
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setInstance(instance);
|
this.setInstance(instance);
|
||||||
@ -327,6 +357,8 @@ export class WAMonitoringService {
|
|||||||
token: instance.token,
|
token: instance.token,
|
||||||
number: instance.number,
|
number: instance.number,
|
||||||
businessId: instance.businessId,
|
businessId: instance.businessId,
|
||||||
|
ownerJid: instance.ownerJid,
|
||||||
|
connectionStatus: instance.connectionStatus as any, // Pass connection status
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -351,6 +383,7 @@ export class WAMonitoringService {
|
|||||||
integration: instance.integration,
|
integration: instance.integration,
|
||||||
token: instance.token,
|
token: instance.token,
|
||||||
businessId: instance.businessId,
|
businessId: instance.businessId,
|
||||||
|
connectionStatus: instance.connectionStatus as any, // Pass connection status
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -361,6 +394,8 @@ export class WAMonitoringService {
|
|||||||
try {
|
try {
|
||||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.REMOVE_INSTANCE, null);
|
await this.waInstances[instanceName]?.sendDataWebhook(Events.REMOVE_INSTANCE, null);
|
||||||
|
|
||||||
|
this.clearDelInstanceTime(instanceName);
|
||||||
|
|
||||||
this.cleaningUp(instanceName);
|
this.cleaningUp(instanceName);
|
||||||
this.cleaningStoreData(instanceName);
|
this.cleaningStoreData(instanceName);
|
||||||
} finally {
|
} finally {
|
||||||
@ -377,6 +412,8 @@ export class WAMonitoringService {
|
|||||||
try {
|
try {
|
||||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
||||||
|
|
||||||
|
this.clearDelInstanceTime(instanceName);
|
||||||
|
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||||
this.waInstances[instanceName]?.clearCacheChatwoot();
|
this.waInstances[instanceName]?.clearCacheChatwoot();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,77 @@ export class TemplateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async edit(
|
||||||
|
instance: InstanceDto,
|
||||||
|
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
|
||||||
|
) {
|
||||||
|
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
|
||||||
|
if (!getInstance) {
|
||||||
|
throw new Error('Instance not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.businessId = getInstance.businessId;
|
||||||
|
this.token = getInstance.token;
|
||||||
|
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
if (typeof data.category === 'string') payload.category = data.category;
|
||||||
|
if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
|
||||||
|
if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
|
||||||
|
if (data.components) payload.components = data.components;
|
||||||
|
|
||||||
|
const response = await this.requestEditTemplate(data.templateId, payload);
|
||||||
|
|
||||||
|
if (!response || response.error) {
|
||||||
|
if (response && response.error) {
|
||||||
|
const metaError = new Error(response.error.message || 'WhatsApp API Error');
|
||||||
|
(metaError as any).template = response.error;
|
||||||
|
throw metaError;
|
||||||
|
}
|
||||||
|
throw new Error('Error to edit template');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(instance: InstanceDto, data: { name: string; hsmId?: string }) {
|
||||||
|
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
|
||||||
|
if (!getInstance) {
|
||||||
|
throw new Error('Instance not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.businessId = getInstance.businessId;
|
||||||
|
this.token = getInstance.token;
|
||||||
|
|
||||||
|
const response = await this.requestDeleteTemplate({ name: data.name, hsm_id: data.hsmId });
|
||||||
|
|
||||||
|
if (!response || response.error) {
|
||||||
|
if (response && response.error) {
|
||||||
|
const metaError = new Error(response.error.message || 'WhatsApp API Error');
|
||||||
|
(metaError as any).template = response.error;
|
||||||
|
throw metaError;
|
||||||
|
}
|
||||||
|
throw new Error('Error to delete template');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Best-effort local cleanup of stored template metadata
|
||||||
|
await this.prismaRepository.template.deleteMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: data.name, instanceId: getInstance.id },
|
||||||
|
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
|
||||||
|
].filter(Boolean) as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
private async requestTemplate(data: any, method: string) {
|
private async requestTemplate(data: any, method: string) {
|
||||||
try {
|
try {
|
||||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||||
@ -116,4 +187,38 @@ export class TemplateService {
|
|||||||
throw new Error(`Connection error: ${e.message}`);
|
throw new Error(`Connection error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async requestEditTemplate(templateId: string, data: any) {
|
||||||
|
try {
|
||||||
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||||
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||||
|
urlServer = `${urlServer}/${version}/${templateId}`;
|
||||||
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||||
|
const result = await axios.post(urlServer, data, { headers });
|
||||||
|
return result.data;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(
|
||||||
|
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
|
||||||
|
);
|
||||||
|
if (e.response?.data) return e.response.data;
|
||||||
|
throw new Error(`Connection error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestDeleteTemplate(params: { name: string; hsm_id?: string }) {
|
||||||
|
try {
|
||||||
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||||
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||||
|
urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`;
|
||||||
|
const headers = { Authorization: `Bearer ${this.token}` };
|
||||||
|
const result = await axios.delete(urlServer, { headers, params });
|
||||||
|
return result.data;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(
|
||||||
|
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
|
||||||
|
);
|
||||||
|
if (e.response?.data) return e.response.data;
|
||||||
|
throw new Error(`Connection error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export declare namespace wa {
|
|||||||
pairingCode?: string;
|
pairingCode?: string;
|
||||||
authState?: { state: AuthenticationState; saveCreds: () => void };
|
authState?: { state: AuthenticationState; saveCreds: () => void };
|
||||||
name?: string;
|
name?: string;
|
||||||
|
ownerJid?: string;
|
||||||
wuid?: string;
|
wuid?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
profilePictureUrl?: string;
|
profilePictureUrl?: string;
|
||||||
|
|||||||
@ -26,8 +26,8 @@ import cors from 'cors';
|
|||||||
import express, { json, NextFunction, Request, Response, urlencoded } from 'express';
|
import express, { json, NextFunction, Request, Response, urlencoded } from 'express';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
function initWA() {
|
async function initWA() {
|
||||||
waMonitor.loadInstance();
|
await waMonitor.loadInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
@ -159,7 +159,9 @@ async function bootstrap() {
|
|||||||
|
|
||||||
server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT));
|
server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT));
|
||||||
|
|
||||||
initWA();
|
initWA().catch((error) => {
|
||||||
|
logger.error('Error loading instances: ' + error);
|
||||||
|
});
|
||||||
|
|
||||||
onUnexpectedError();
|
onUnexpectedError();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { socksDispatcher } from 'fetch-socks';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import { ProxyAgent } from 'undici';
|
||||||
|
|
||||||
type Proxy = {
|
type Proxy = {
|
||||||
host: string;
|
host: string;
|
||||||
@ -17,12 +19,23 @@ function selectProxyAgent(proxyUrl: string): HttpsProxyAgent<string> | SocksProx
|
|||||||
// the end so, we add the protocol constants without the `:` to avoid confusion.
|
// the end so, we add the protocol constants without the `:` to avoid confusion.
|
||||||
const PROXY_HTTP_PROTOCOL = 'http:';
|
const PROXY_HTTP_PROTOCOL = 'http:';
|
||||||
const PROXY_SOCKS_PROTOCOL = 'socks:';
|
const PROXY_SOCKS_PROTOCOL = 'socks:';
|
||||||
|
const PROXY_SOCKS5_PROTOCOL = 'socks5:';
|
||||||
|
|
||||||
switch (url.protocol) {
|
switch (url.protocol) {
|
||||||
case PROXY_HTTP_PROTOCOL:
|
case PROXY_HTTP_PROTOCOL:
|
||||||
return new HttpsProxyAgent(url);
|
return new HttpsProxyAgent(url);
|
||||||
case PROXY_SOCKS_PROTOCOL:
|
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:
|
default:
|
||||||
throw new Error(`Unsupported proxy protocol: ${url.protocol}`);
|
throw new Error(`Unsupported proxy protocol: ${url.protocol}`);
|
||||||
}
|
}
|
||||||
@ -42,3 +55,57 @@ export function makeProxyAgent(proxy: Proxy | string): HttpsProxyAgent<string> |
|
|||||||
|
|
||||||
return selectProxyAgent(proxyUrl);
|
return selectProxyAgent(proxyUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeProxyAgentUndici(proxy: Proxy | string): ProxyAgent {
|
||||||
|
let proxyUrl: string;
|
||||||
|
let protocol: string;
|
||||||
|
|
||||||
|
if (typeof proxy === 'string') {
|
||||||
|
const url = new URL(proxy);
|
||||||
|
protocol = url.protocol.replace(':', '');
|
||||||
|
proxyUrl = proxy;
|
||||||
|
} else {
|
||||||
|
const { host, password, port, protocol: proto, username } = proxy;
|
||||||
|
protocol = (proto || 'http').replace(':', '');
|
||||||
|
|
||||||
|
if (protocol === 'socks') {
|
||||||
|
protocol = 'socks5';
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = username && password ? `${username}:${password}@` : '';
|
||||||
|
proxyUrl = `${protocol}://${auth}${host}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol = protocol.toLowerCase();
|
||||||
|
|
||||||
|
const PROXY_HTTP_PROTOCOL = 'http';
|
||||||
|
const PROXY_HTTPS_PROTOCOL = 'https';
|
||||||
|
const PROXY_SOCKS4_PROTOCOL = 'socks4';
|
||||||
|
const PROXY_SOCKS5_PROTOCOL = 'socks5';
|
||||||
|
|
||||||
|
switch (protocol) {
|
||||||
|
case PROXY_HTTP_PROTOCOL:
|
||||||
|
case PROXY_HTTPS_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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { prismaRepository } from '@api/server.module';
|
import { prismaRepository } from '@api/server.module';
|
||||||
import { configService, Database } from '@config/env.config';
|
import { configService, Database } from '@config/env.config';
|
||||||
|
import { Logger } from '@config/logger.config';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const logger = new Logger('OnWhatsappCache');
|
||||||
|
|
||||||
function getAvailableNumbers(remoteJid: string) {
|
function getAvailableNumbers(remoteJid: string) {
|
||||||
const numbersAvailable: string[] = [];
|
const numbersAvailable: string[] = [];
|
||||||
|
|
||||||
@ -11,6 +14,11 @@ function getAvailableNumbers(remoteJid: string) {
|
|||||||
|
|
||||||
const [number, domain] = remoteJid.split('@');
|
const [number, domain] = remoteJid.split('@');
|
||||||
|
|
||||||
|
// TODO: Se já for @lid, retornar apenas ele mesmo SEM adicionar @domain novamente
|
||||||
|
if (domain === 'lid' || domain === 'g.us') {
|
||||||
|
return [remoteJid]; // Retorna direto para @lid e @g.us
|
||||||
|
}
|
||||||
|
|
||||||
// Brazilian numbers
|
// Brazilian numbers
|
||||||
if (remoteJid.startsWith('55')) {
|
if (remoteJid.startsWith('55')) {
|
||||||
const numberWithDigit =
|
const numberWithDigit =
|
||||||
@ -47,36 +55,128 @@ function getAvailableNumbers(remoteJid: string) {
|
|||||||
numbersAvailable.push(remoteJid);
|
numbersAvailable.push(remoteJid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Adiciona @domain apenas para números que não são @lid
|
||||||
return numbersAvailable.map((number) => `${number}@${domain}`);
|
return numbersAvailable.map((number) => `${number}@${domain}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISaveOnWhatsappCacheParams {
|
interface ISaveOnWhatsappCacheParams {
|
||||||
remoteJid: string;
|
remoteJid: string;
|
||||||
lid?: string;
|
remoteJidAlt?: string;
|
||||||
|
lid?: 'lid' | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeJid(jid: string | null | undefined): string | null {
|
||||||
|
if (!jid) return null;
|
||||||
|
return jid.startsWith('+') ? jid.slice(1) : jid;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
||||||
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
if (!configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
|
||||||
const upsertsQuery = data.map((item) => {
|
return;
|
||||||
const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
|
|
||||||
const numbersAvailable = getAvailableNumbers(remoteJid);
|
|
||||||
|
|
||||||
return prismaRepository.isOnWhatsapp.upsert({
|
|
||||||
create: {
|
|
||||||
remoteJid: remoteJid,
|
|
||||||
jidOptions: numbersAvailable.join(','),
|
|
||||||
lid: item.lid,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
jidOptions: numbersAvailable.join(','),
|
|
||||||
lid: item.lid,
|
|
||||||
},
|
|
||||||
where: { remoteJid: remoteJid },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await prismaRepository.$transaction(upsertsQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Processa todos os itens em paralelo para melhor performance
|
||||||
|
const processingPromises = data.map(async (item) => {
|
||||||
|
try {
|
||||||
|
const remoteJid = normalizeJid(item.remoteJid);
|
||||||
|
if (!remoteJid) {
|
||||||
|
logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const altJidNormalized = normalizeJid(item.remoteJidAlt);
|
||||||
|
const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null;
|
||||||
|
|
||||||
|
const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial
|
||||||
|
if (lidAltJid) {
|
||||||
|
baseJids.push(lidAltJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid));
|
||||||
|
|
||||||
|
// 1. Busca entrada por jidOptions e também remoteJid
|
||||||
|
// Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro:
|
||||||
|
// 'Unique constraint failed on the fields: (`remoteJid`)'
|
||||||
|
// Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us')
|
||||||
|
const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })),
|
||||||
|
{ remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.verbose(
|
||||||
|
`[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Unifica todos os JIDs usando um Set para garantir valores únicos
|
||||||
|
const finalJidOptions = new Set(expandedJids);
|
||||||
|
|
||||||
|
if (lidAltJid) {
|
||||||
|
finalJidOptions.add(lidAltJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingRecord?.jidOptions) {
|
||||||
|
existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prepara o payload final
|
||||||
|
// Ordena os JIDs para garantir consistência na string final
|
||||||
|
const sortedJidOptions = [...finalJidOptions].sort();
|
||||||
|
const newJidOptionsString = sortedJidOptions.join(',');
|
||||||
|
const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null;
|
||||||
|
|
||||||
|
const dataPayload = {
|
||||||
|
remoteJid: remoteJid,
|
||||||
|
jidOptions: newJidOptionsString,
|
||||||
|
lid: newLid,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Decide entre Criar ou Atualizar
|
||||||
|
if (existingRecord) {
|
||||||
|
// Compara a string de JIDs ordenada existente com a nova
|
||||||
|
const existingJidOptionsString = existingRecord.jidOptions
|
||||||
|
? existingRecord.jidOptions.split(',').sort().join(',')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const isDataSame =
|
||||||
|
existingRecord.remoteJid === dataPayload.remoteJid &&
|
||||||
|
existingJidOptionsString === dataPayload.jidOptions &&
|
||||||
|
existingRecord.lid === dataPayload.lid;
|
||||||
|
|
||||||
|
if (isDataSame) {
|
||||||
|
logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`);
|
||||||
|
return; // Pula para o próximo item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Os dados são diferentes, então atualiza
|
||||||
|
logger.verbose(
|
||||||
|
`[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
||||||
|
);
|
||||||
|
await prismaRepository.isOnWhatsapp.update({
|
||||||
|
where: { id: existingRecord.id },
|
||||||
|
data: dataPayload,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Cria nova entrada
|
||||||
|
logger.verbose(
|
||||||
|
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
||||||
|
);
|
||||||
|
await prismaRepository.isOnWhatsapp.create({
|
||||||
|
data: dataPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Loga o erro mas não para a execução dos outros promises
|
||||||
|
logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `);
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Espera todas as operações paralelas terminarem
|
||||||
|
await Promise.allSettled(processingPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOnWhatsappCache(remoteJids: string[]) {
|
export async function getOnWhatsappCache(remoteJids: string[]) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { prismaRepository } from '@api/server.module';
|
import { prismaRepository } from '@api/server.module';
|
||||||
import { CacheService } from '@api/services/cache.service';
|
import { CacheService } from '@api/services/cache.service';
|
||||||
import { CacheConf, configService } from '@config/env.config';
|
import { CacheConf, configService } from '@config/env.config';
|
||||||
|
import { Logger } from '@config/logger.config';
|
||||||
import { INSTANCE_DIR } from '@config/path.config';
|
import { INSTANCE_DIR } from '@config/path.config';
|
||||||
import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys';
|
import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
@ -73,12 +74,15 @@ async function fileExists(file: string): Promise<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = new Logger('useMultiFileAuthStatePrisma');
|
||||||
|
|
||||||
export default async function useMultiFileAuthStatePrisma(
|
export default async function useMultiFileAuthStatePrisma(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
cache: CacheService,
|
cache: CacheService,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
state: AuthenticationState;
|
state: AuthenticationState;
|
||||||
saveCreds: () => Promise<void>;
|
saveCreds: () => Promise<void>;
|
||||||
|
removeCreds: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
const localFolder = path.join(INSTANCE_DIR, sessionId);
|
const localFolder = path.join(INSTANCE_DIR, sessionId);
|
||||||
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
|
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
|
||||||
@ -142,6 +146,26 @@ export default async function useMultiFileAuthStatePrisma(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeCreds(): Promise<any> {
|
||||||
|
const cacheConfig = configService.get<CacheConf>('CACHE');
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
try {
|
||||||
|
if (cacheConfig.REDIS.ENABLED) {
|
||||||
|
await cache.delete(sessionId);
|
||||||
|
logger.info({ action: 'redis.delete', sessionId });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ action: 'redis.delete', sessionId, err });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ action: 'auth.key.delete', sessionId });
|
||||||
|
|
||||||
|
await deleteAuthKey(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
let creds = await readData('creds');
|
let creds = await readData('creds');
|
||||||
if (!creds) {
|
if (!creds) {
|
||||||
creds = initAuthCreds();
|
creds = initAuthCreds();
|
||||||
@ -183,5 +207,7 @@ export default async function useMultiFileAuthStatePrisma(
|
|||||||
saveCreds: () => {
|
saveCreds: () => {
|
||||||
return writeData(creds, 'creds');
|
return writeData(creds, 'creds');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeCreds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,11 @@ import { Logger } from '@config/logger.config';
|
|||||||
import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
|
import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
|
||||||
import { isNotEmpty } from 'class-validator';
|
import { isNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
export type AuthState = { state: AuthenticationState; saveCreds: () => Promise<void> };
|
export type AuthState = {
|
||||||
|
state: AuthenticationState;
|
||||||
|
saveCreds: () => Promise<void>;
|
||||||
|
removeCreds: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
export class AuthStateProvider {
|
export class AuthStateProvider {
|
||||||
constructor(private readonly providerFiles: ProviderFiles) {}
|
constructor(private readonly providerFiles: ProviderFiles) {}
|
||||||
@ -86,6 +90,18 @@ export class AuthStateProvider {
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeCreds = async () => {
|
||||||
|
const [response, error] = await this.providerFiles.removeSession(instance);
|
||||||
|
if (error) {
|
||||||
|
// this.logger.error(['removeData', error?.message, error?.stack]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ action: 'remove.session', instance, response });
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -126,6 +142,10 @@ export class AuthStateProvider {
|
|||||||
saveCreds: async () => {
|
saveCreds: async () => {
|
||||||
return await writeData(creds, 'creds');
|
return await writeData(creds, 'creds');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeCreds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = new Logger('useMultiFileAuthStatePrisma');
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export async function useMultiFileAuthStateRedisDb(
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
state: AuthenticationState;
|
state: AuthenticationState;
|
||||||
saveCreds: () => Promise<void>;
|
saveCreds: () => Promise<void>;
|
||||||
|
removeCreds: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
const logger = new Logger('useMultiFileAuthStateRedisDb');
|
const logger = new Logger('useMultiFileAuthStateRedisDb');
|
||||||
|
|
||||||
@ -36,6 +37,16 @@ export async function useMultiFileAuthStateRedisDb(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function removeCreds(): Promise<any> {
|
||||||
|
try {
|
||||||
|
logger.warn({ action: 'redis.delete', instanceName });
|
||||||
|
|
||||||
|
return await cache.delete(instanceName);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -76,5 +87,7 @@ export async function useMultiFileAuthStateRedisDb(
|
|||||||
saveCreds: async () => {
|
saveCreds: async () => {
|
||||||
return await writeData(creds, 'creds');
|
return await writeData(creds, 'creds');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeCreds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -195,8 +195,9 @@ export const contactValidateSchema: JSONSchema7 = {
|
|||||||
_id: { type: 'string', minLength: 1 },
|
_id: { type: 'string', minLength: 1 },
|
||||||
pushName: { type: 'string', minLength: 1 },
|
pushName: { type: 'string', minLength: 1 },
|
||||||
id: { type: 'string', minLength: 1 },
|
id: { type: 'string', minLength: 1 },
|
||||||
|
remoteJid: { type: 'string', minLength: 1 },
|
||||||
},
|
},
|
||||||
...isNotEmpty('_id', 'id', 'pushName'),
|
...isNotEmpty('_id', 'id', 'pushName', 'remoteJid'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/validate/templateDelete.schema.ts
Normal file
32
src/validate/templateDelete.schema.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||||
|
const properties: Record<string, unknown> = {};
|
||||||
|
propertyNames.forEach(
|
||||||
|
(property) =>
|
||||||
|
(properties[property] = {
|
||||||
|
minLength: 1,
|
||||||
|
description: `The "${property}" cannot be empty`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
if: {
|
||||||
|
propertyNames: {
|
||||||
|
enum: [...propertyNames],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
then: { properties },
|
||||||
|
} as JSONSchema7;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const templateDeleteSchema: JSONSchema7 = {
|
||||||
|
$id: v4(),
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
hsmId: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['name'],
|
||||||
|
...isNotEmpty('name'),
|
||||||
|
};
|
||||||
35
src/validate/templateEdit.schema.ts
Normal file
35
src/validate/templateEdit.schema.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||||
|
const properties: Record<string, unknown> = {};
|
||||||
|
propertyNames.forEach(
|
||||||
|
(property) =>
|
||||||
|
(properties[property] = {
|
||||||
|
minLength: 1,
|
||||||
|
description: `The "${property}" cannot be empty`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
if: {
|
||||||
|
propertyNames: {
|
||||||
|
enum: [...propertyNames],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
then: { properties },
|
||||||
|
} as JSONSchema7;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const templateEditSchema: JSONSchema7 = {
|
||||||
|
$id: v4(),
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
templateId: { type: 'string' },
|
||||||
|
category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
|
||||||
|
allowCategoryChange: { type: 'boolean' },
|
||||||
|
ttl: { type: 'number' },
|
||||||
|
components: { type: 'array' },
|
||||||
|
},
|
||||||
|
required: ['templateId'],
|
||||||
|
...isNotEmpty('templateId'),
|
||||||
|
};
|
||||||
@ -8,5 +8,7 @@ export * from './message.schema';
|
|||||||
export * from './proxy.schema';
|
export * from './proxy.schema';
|
||||||
export * from './settings.schema';
|
export * from './settings.schema';
|
||||||
export * from './template.schema';
|
export * from './template.schema';
|
||||||
|
export * from './templateDelete.schema';
|
||||||
|
export * from './templateEdit.schema';
|
||||||
export * from '@api/integrations/chatbot/chatbot.schema';
|
export * from '@api/integrations/chatbot/chatbot.schema';
|
||||||
export * from '@api/integrations/event/event.schema';
|
export * from '@api/integrations/event/event.schema';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user