mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-12 19:39:36 -06:00
Compare commits
200 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 | ||
|
|
d48fbc3a4e | ||
|
|
cdf06666a1 | ||
|
|
5254928887 | ||
|
|
8468690d37 | ||
|
|
bdd9257c47 | ||
|
|
d6834c8741 | ||
|
|
164beddb39 | ||
|
|
4991f1dc37 | ||
|
|
1b1e3b3e9d | ||
|
|
563ca2dd22 | ||
|
|
4e44bfb222 | ||
|
|
9edd600513 | ||
|
|
501b06d133 | ||
|
|
dc530285d5 | ||
|
|
8775cdf036 | ||
|
|
6ad33df879 | ||
|
|
633d0b4c45 | ||
|
|
82c0eadf7c | ||
|
|
1756abf1e6 | ||
|
|
a2f48030dc | ||
|
|
3214a9fb5b | ||
|
|
4b89e3f987 | ||
|
|
72622dca31 | ||
|
|
d73b72b67e | ||
|
|
20eef33df3 | ||
|
|
37571c03b4 | ||
|
|
017949458b | ||
|
|
2feaf1c74e | ||
|
|
4b043cb4b8 | ||
|
|
b0d261b305 | ||
|
|
c041986e26 | ||
|
|
0976109d27 | ||
|
|
b808dda33b | ||
|
|
98b7f15a43 | ||
|
|
94ddc0dfbe | ||
|
|
d4b0cfd2ba | ||
|
|
a5a46dc72a | ||
|
|
e13434804c | ||
|
|
53cd7d5d13 | ||
|
|
bedfb019aa | ||
|
|
6e1d027750 | ||
|
|
fb1fa4d91a | ||
|
|
57ea6707bc | ||
|
|
ad8df44236 | ||
|
|
c132379b3a | ||
|
|
f7862637b1 | ||
|
|
0d8e8bc0fb | ||
|
|
b62917e80f | ||
|
|
eeb324227b | ||
|
|
c31b62fb3d | ||
|
|
22465c0a56 | ||
|
|
da6f1bd540 | ||
|
|
069786b9fe | ||
|
|
bd0c43feac | ||
|
|
5dc1d02d0a | ||
|
|
8697329f71 | ||
|
|
58b5561f72 | ||
|
|
093515555d | ||
|
|
d8268b0eb1 | ||
|
|
4585850741 | ||
|
|
6c150eed6d | ||
|
|
78c7b96f0f | ||
|
|
dfea584aa7 | ||
|
|
6c5b056615 | ||
|
|
d8b4378163 | ||
|
|
838cc14531 | ||
|
|
878da12fa4 | ||
|
|
10a2b60595 | ||
|
|
b0ca79cd11 | ||
|
|
71eb189a6d | ||
|
|
407d254cf7 | ||
|
|
5f44da61fb | ||
|
|
41a36bbb19 | ||
|
|
8ab41fcfc9 | ||
|
|
5e08628d89 | ||
|
|
f0c6300599 | ||
|
|
24c339343f | ||
|
|
ddbaf2335a | ||
|
|
20c8a2ff0e | ||
|
|
e623269a18 | ||
|
|
0363fa979d | ||
|
|
b640329cf8 | ||
|
|
f72b1f7717 | ||
|
|
ed4c8868a0 | ||
|
|
06081f6502 | ||
|
|
8e51ae63ae |
54
.env.example
54
.env.example
@ -190,6 +190,60 @@ PUSHER_EVENTS_CALL=true
|
|||||||
PUSHER_EVENTS_TYPEBOT_START=false
|
PUSHER_EVENTS_TYPEBOT_START=false
|
||||||
PUSHER_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
PUSHER_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||||
|
|
||||||
|
# Kafka - Environment variables
|
||||||
|
KAFKA_ENABLED=false
|
||||||
|
KAFKA_CLIENT_ID=evolution-api
|
||||||
|
KAFKA_BROKERS=localhost:9092
|
||||||
|
KAFKA_CONNECTION_TIMEOUT=3000
|
||||||
|
KAFKA_REQUEST_TIMEOUT=30000
|
||||||
|
# Global events - By enabling this variable, events from all instances are sent to global Kafka topics.
|
||||||
|
KAFKA_GLOBAL_ENABLED=false
|
||||||
|
KAFKA_CONSUMER_GROUP_ID=evolution-api-consumers
|
||||||
|
KAFKA_TOPIC_PREFIX=evolution
|
||||||
|
KAFKA_NUM_PARTITIONS=1
|
||||||
|
KAFKA_REPLICATION_FACTOR=1
|
||||||
|
KAFKA_AUTO_CREATE_TOPICS=false
|
||||||
|
# Choose the events you want to send to Kafka
|
||||||
|
KAFKA_EVENTS_APPLICATION_STARTUP=false
|
||||||
|
KAFKA_EVENTS_INSTANCE_CREATE=false
|
||||||
|
KAFKA_EVENTS_INSTANCE_DELETE=false
|
||||||
|
KAFKA_EVENTS_QRCODE_UPDATED=false
|
||||||
|
KAFKA_EVENTS_MESSAGES_SET=false
|
||||||
|
KAFKA_EVENTS_MESSAGES_UPSERT=false
|
||||||
|
KAFKA_EVENTS_MESSAGES_EDITED=false
|
||||||
|
KAFKA_EVENTS_MESSAGES_UPDATE=false
|
||||||
|
KAFKA_EVENTS_MESSAGES_DELETE=false
|
||||||
|
KAFKA_EVENTS_SEND_MESSAGE=false
|
||||||
|
KAFKA_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||||
|
KAFKA_EVENTS_CONTACTS_SET=false
|
||||||
|
KAFKA_EVENTS_CONTACTS_UPSERT=false
|
||||||
|
KAFKA_EVENTS_CONTACTS_UPDATE=false
|
||||||
|
KAFKA_EVENTS_PRESENCE_UPDATE=false
|
||||||
|
KAFKA_EVENTS_CHATS_SET=false
|
||||||
|
KAFKA_EVENTS_CHATS_UPSERT=false
|
||||||
|
KAFKA_EVENTS_CHATS_UPDATE=false
|
||||||
|
KAFKA_EVENTS_CHATS_DELETE=false
|
||||||
|
KAFKA_EVENTS_GROUPS_UPSERT=false
|
||||||
|
KAFKA_EVENTS_GROUPS_UPDATE=false
|
||||||
|
KAFKA_EVENTS_GROUP_PARTICIPANTS_UPDATE=false
|
||||||
|
KAFKA_EVENTS_CONNECTION_UPDATE=false
|
||||||
|
KAFKA_EVENTS_LABELS_EDIT=false
|
||||||
|
KAFKA_EVENTS_LABELS_ASSOCIATION=false
|
||||||
|
KAFKA_EVENTS_CALL=false
|
||||||
|
KAFKA_EVENTS_TYPEBOT_START=false
|
||||||
|
KAFKA_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||||
|
# SASL Authentication (optional)
|
||||||
|
KAFKA_SASL_ENABLED=false
|
||||||
|
KAFKA_SASL_MECHANISM=plain
|
||||||
|
KAFKA_SASL_USERNAME=
|
||||||
|
KAFKA_SASL_PASSWORD=
|
||||||
|
# SSL Configuration (optional)
|
||||||
|
KAFKA_SSL_ENABLED=false
|
||||||
|
KAFKA_SSL_REJECT_UNAUTHORIZED=true
|
||||||
|
KAFKA_SSL_CA=
|
||||||
|
KAFKA_SSL_KEY=
|
||||||
|
KAFKA_SSL_CERT=
|
||||||
|
|
||||||
# WhatsApp Business API - Environment variables
|
# WhatsApp Business API - Environment variables
|
||||||
# Token used to validate the webhook on the Facebook APP
|
# Token used to validate the webhook on the Facebook APP
|
||||||
WA_BUSINESS_TOKEN_WEBHOOK=evolution
|
WA_BUSINESS_TOKEN_WEBHOOK=evolution
|
||||||
|
|||||||
@ -26,7 +26,7 @@ module.exports = {
|
|||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
'import/first': 'error',
|
'import/first': 'error',
|
||||||
'import/no-duplicates': 'error',
|
'import/no-duplicates': 'error',
|
||||||
'simple-import-sort/imports': 'error',
|
'simple-import-sort/imports': 'error',
|
||||||
|
|||||||
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.3]
|
- 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:
|
||||||
|
|||||||
2
.github/workflows/check_code_quality.yml
vendored
2
.github/workflows/check_code_quality.yml
vendored
@ -13,6 +13,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v5
|
||||||
|
|||||||
2
.github/workflows/publish_docker_image.yml
vendored
2
.github/workflows/publish_docker_image.yml
vendored
@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
4
.github/workflows/security.yml
vendored
4
.github/workflows/security.yml
vendored
@ -26,6 +26,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
@ -47,5 +49,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v4
|
uses: actions/dependency-review-action@v4
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "evolution-manager-v2"]
|
||||||
|
path = evolution-manager-v2
|
||||||
|
url = https://github.com/EvolutionAPI/evolution-manager-v2.git
|
||||||
293
CHANGELOG.md
293
CHANGELOG.md
@ -1,3 +1,296 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Chatwoot Enhancements**: Comprehensive improvements to message handling, editing, deletion and i18n
|
||||||
|
* **Participants Data**: Add participantsData field maintaining backward compatibility for group participants
|
||||||
|
* **LID to Phone Number**: Convert LID to phoneNumber on group participants
|
||||||
|
* **Docker Configurations**: Add Kafka and frontend services to Docker configurations
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* **Kafka Migration**: Fixed PostgreSQL migration error for Kafka integration
|
||||||
|
- Corrected table reference from `"public"."Instance"` to `"Instance"` in foreign key constraint
|
||||||
|
- Fixed `ERROR: relation "public.Instance" does not exist` issue in migration `20250918182355_add_kafka_integration`
|
||||||
|
- Aligned table naming convention with other Evolution API migrations for consistency
|
||||||
|
- Resolved database migration failure that prevented Kafka integration setup
|
||||||
|
* **Update Baileys Version**: v7.0.0-rc.5 with compatibility fixes
|
||||||
|
- Fixed assertSessions signature compatibility using type assertion
|
||||||
|
- Fixed incompatibility in voice call (wavoip) with new Baileys version
|
||||||
|
- Handle undefined status in update by defaulting to 'DELETED'
|
||||||
|
* **Chatwoot Improvements**: Multiple fixes for enhanced reliability
|
||||||
|
- Correct chatId extraction for non-group JIDs
|
||||||
|
- Resolve webhook timeout on deletion with 5+ images
|
||||||
|
- Improve error handling in Chatwoot messages
|
||||||
|
- Adjust conversation verification logic and cache
|
||||||
|
- Optimize conversation reopening logic and connection notification
|
||||||
|
- Fix conversation reopening and connection loop
|
||||||
|
* **Baileys Message Handling**: Enhanced message processing
|
||||||
|
- Add warning log for messages not found
|
||||||
|
- Fix message verification in Baileys service
|
||||||
|
- Simplify linkPreview handling in BaileysStartupService
|
||||||
|
* **Media Validation**: Fix media content validation
|
||||||
|
* **PostgreSQL Connection**: Refactor connection with PostgreSQL and improve message handling
|
||||||
|
|
||||||
|
### Code Quality & Refactoring
|
||||||
|
|
||||||
|
* **Exponential Backoff**: Implement exponential backoff patterns and extract magic numbers to constants
|
||||||
|
* **TypeScript Build**: Update TypeScript build process and dependencies
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# 2.3.4 (2025-09-23)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Kafka Integration**: Added Apache Kafka event integration for real-time event streaming
|
||||||
|
- New Kafka controller, router, and schema for event publishing
|
||||||
|
- Support for instance-specific and global event topics
|
||||||
|
- Configurable SASL/SSL authentication and connection settings
|
||||||
|
- Auto-creation of topics with configurable partitions and replication
|
||||||
|
- Consumer group management for reliable event processing
|
||||||
|
- Integration with existing event manager for seamless event distribution
|
||||||
|
|
||||||
|
* **Evolution Manager v2 Open Source**: Evolution Manager v2 is now available as open source
|
||||||
|
- Added as git submodule with HTTPS URL for easy access
|
||||||
|
- Complete open source setup with Apache 2.0 license + Evolution API custom conditions
|
||||||
|
- GitHub templates for issues, pull requests, and workflows
|
||||||
|
- Comprehensive documentation and contribution guidelines
|
||||||
|
- Docker support for development and production environments
|
||||||
|
- CI/CD workflows for code quality, security audits, and automated builds
|
||||||
|
- Multi-language support (English, Portuguese, Spanish, French)
|
||||||
|
- Modern React + TypeScript + Vite frontend with Tailwind CSS
|
||||||
|
|
||||||
|
* **EvolutionBot Enhancements**: Improved EvolutionBot functionality and message handling
|
||||||
|
- Implemented splitMessages functionality for better message segmentation
|
||||||
|
- Added linkPreview support for enhanced message presentation
|
||||||
|
- Centralized split logic across chatbot services for consistency
|
||||||
|
- Enhanced message formatting and delivery capabilities
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* **MySQL Schema**: Fixed invalid default value errors for `createdAt` fields in `Evoai` and `EvoaiSetting` models
|
||||||
|
- Changed `@default(now())` to `@default(dbgenerated("CURRENT_TIMESTAMP"))` for MySQL compatibility
|
||||||
|
- Added missing relation fields (`N8n`, `N8nSetting`, `Evoai`, `EvoaiSetting`) in Instance model
|
||||||
|
- Resolved Prisma schema validation errors for MySQL provider
|
||||||
|
|
||||||
|
* **Prisma Schema Validation**: Fixed `instanceName` field error in message creation
|
||||||
|
- Removed invalid `instanceName` field from message objects before database insertion
|
||||||
|
- Resolved `Unknown argument 'instanceName'` Prisma validation error
|
||||||
|
- Streamlined message data structure to match Prisma schema requirements
|
||||||
|
|
||||||
|
* **Media Message Processing**: Enhanced media handling across chatbot services
|
||||||
|
- Fixed base64 conversion in EvoAI service for proper image processing
|
||||||
|
- Converted ArrayBuffer to base64 string using `Buffer.from().toString('base64')`
|
||||||
|
- Improved media URL handling and base64 encoding for better chatbot integration
|
||||||
|
- Enhanced image message detection and processing workflow
|
||||||
|
|
||||||
|
* **Evolution Manager v2 Linting**: Resolved ESLint configuration conflicts
|
||||||
|
- Disabled conflicting Prettier rules in ESLint configuration
|
||||||
|
- Added comprehensive rule overrides for TypeScript and React patterns
|
||||||
|
- Fixed import ordering and code formatting issues
|
||||||
|
- Updated security vulnerabilities in dependencies (Vite, esbuild)
|
||||||
|
|
||||||
|
### Code Quality & Refactoring
|
||||||
|
|
||||||
|
* **Chatbot Services**: Streamlined media message handling across all chatbot integrations
|
||||||
|
- Standardized base64 and mediaUrl processing patterns
|
||||||
|
- Improved code readability and maintainability in media handling logic
|
||||||
|
- Enhanced error handling for media download and conversion processes
|
||||||
|
- Unified image message detection across different chatbot services
|
||||||
|
|
||||||
|
* **Database Operations**: Improved data consistency and validation
|
||||||
|
- Enhanced Prisma schema compliance across all message operations
|
||||||
|
- Removed redundant instance name references for better data integrity
|
||||||
|
- Optimized message creation workflow with proper field validation
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
* Added comprehensive Kafka configuration options:
|
||||||
|
- `KAFKA_ENABLED`, `KAFKA_CLIENT_ID`, `KAFKA_BROKERS`
|
||||||
|
- `KAFKA_CONSUMER_GROUP_ID`, `KAFKA_TOPIC_PREFIX`
|
||||||
|
- `KAFKA_SASL_*` and `KAFKA_SSL_*` for authentication
|
||||||
|
- `KAFKA_EVENTS_*` for event type configuration
|
||||||
|
|
||||||
# 2.3.3 (2025-09-18)
|
# 2.3.3 (2025-09-18)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
51
Docker/kafka/docker-compose.yaml
Normal file
51
Docker/kafka/docker-compose.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
zookeeper:
|
||||||
|
container_name: zookeeper
|
||||||
|
image: confluentinc/cp-zookeeper:7.5.0
|
||||||
|
environment:
|
||||||
|
- ZOOKEEPER_CLIENT_PORT=2181
|
||||||
|
- ZOOKEEPER_TICK_TIME=2000
|
||||||
|
- ZOOKEEPER_SYNC_LIMIT=2
|
||||||
|
volumes:
|
||||||
|
- zookeeper_data:/var/lib/zookeeper/
|
||||||
|
ports:
|
||||||
|
- 2181:2181
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
container_name: kafka
|
||||||
|
image: confluentinc/cp-kafka:7.5.0
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
environment:
|
||||||
|
- KAFKA_BROKER_ID=1
|
||||||
|
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
|
||||||
|
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,OUTSIDE:PLAINTEXT
|
||||||
|
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,OUTSIDE://host.docker.internal:9094
|
||||||
|
- KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT
|
||||||
|
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
|
||||||
|
- KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1
|
||||||
|
- KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1
|
||||||
|
- KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0
|
||||||
|
- KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
|
||||||
|
- KAFKA_LOG_RETENTION_HOURS=168
|
||||||
|
- KAFKA_LOG_SEGMENT_BYTES=1073741824
|
||||||
|
- KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS=300000
|
||||||
|
- KAFKA_COMPRESSION_TYPE=gzip
|
||||||
|
ports:
|
||||||
|
- 29092:29092
|
||||||
|
- 9092:9092
|
||||||
|
- 9094:9094
|
||||||
|
volumes:
|
||||||
|
- kafka_data:/var/lib/kafka/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
zookeeper_data:
|
||||||
|
kafka_data:
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
evolution-net:
|
||||||
|
name: evolution-net
|
||||||
|
driver: bridge
|
||||||
@ -2,7 +2,7 @@ version: "3.7"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
evolution_v2:
|
evolution_v2:
|
||||||
image: evoapicloud/evolution-api:v2.3.1
|
image: evoapicloud/evolution-api:v2.3.7
|
||||||
volumes:
|
volumes:
|
||||||
- evolution_instances:/evolution/instances
|
- evolution_instances:/evolution/instances
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@ -17,5 +17,5 @@ b. Your contributed code may be used for commercial purposes, including but not
|
|||||||
|
|
||||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
© 2024 Evolution API
|
© 2025 Evolution API
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,9 @@ Evolution API supports various integrations to enhance its functionality. Below
|
|||||||
- [RabbitMQ](https://www.rabbitmq.com/):
|
- [RabbitMQ](https://www.rabbitmq.com/):
|
||||||
- Receive events from the Evolution API via RabbitMQ.
|
- Receive events from the Evolution API via RabbitMQ.
|
||||||
|
|
||||||
|
- [Apache Kafka](https://kafka.apache.org/):
|
||||||
|
- Receive events from the Evolution API via Apache Kafka for real-time event streaming and processing.
|
||||||
|
|
||||||
- [Amazon SQS](https://aws.amazon.com/pt/sqs/):
|
- [Amazon SQS](https://aws.amazon.com/pt/sqs/):
|
||||||
- Receive events from the Evolution API via Amazon SQS.
|
- Receive events from the Evolution API via Amazon SQS.
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,16 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- 8080
|
- 8080
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: evolution_frontend
|
||||||
|
image: evolution/manager:local
|
||||||
|
build: ./evolution-manager-v2
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
networks:
|
||||||
|
- evolution-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
evolution_instances:
|
evolution_instances:
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ version: "3.8"
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
container_name: evolution_api
|
container_name: evolution_api
|
||||||
image: evolution/api:metrics
|
image: evoapicloud/evolution-api:latest
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
@ -20,6 +20,15 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "8080"
|
- "8080"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: evolution_frontend
|
||||||
|
image: evoapicloud/evolution-manager:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
networks:
|
||||||
|
- evolution-net
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: evolution_redis
|
container_name: evolution_redis
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
|
|||||||
302
env.example
Normal file
302
env.example
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# ===========================================
|
||||||
|
# EVOLUTION API - CONFIGURAÇÃO DE AMBIENTE
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SERVIDOR
|
||||||
|
# ===========================================
|
||||||
|
SERVER_NAME=evolution
|
||||||
|
SERVER_TYPE=http
|
||||||
|
SERVER_PORT=8080
|
||||||
|
SERVER_URL=http://localhost:8080
|
||||||
|
SERVER_DISABLE_DOCS=false
|
||||||
|
SERVER_DISABLE_MANAGER=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# CORS
|
||||||
|
# ===========================================
|
||||||
|
CORS_ORIGIN=*
|
||||||
|
CORS_METHODS=POST,GET,PUT,DELETE
|
||||||
|
CORS_CREDENTIALS=true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SSL (opcional)
|
||||||
|
# ===========================================
|
||||||
|
SSL_CONF_PRIVKEY=
|
||||||
|
SSL_CONF_FULLCHAIN=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# BANCO DE DADOS
|
||||||
|
# ===========================================
|
||||||
|
DATABASE_PROVIDER=postgresql
|
||||||
|
DATABASE_CONNECTION_URI=postgresql://username:password@localhost:5432/evolution_api
|
||||||
|
DATABASE_CONNECTION_CLIENT_NAME=evolution
|
||||||
|
|
||||||
|
# Configurações de salvamento de dados
|
||||||
|
DATABASE_SAVE_DATA_INSTANCE=true
|
||||||
|
DATABASE_SAVE_DATA_NEW_MESSAGE=true
|
||||||
|
DATABASE_SAVE_MESSAGE_UPDATE=true
|
||||||
|
DATABASE_SAVE_DATA_CONTACTS=true
|
||||||
|
DATABASE_SAVE_DATA_CHATS=true
|
||||||
|
DATABASE_SAVE_DATA_HISTORIC=true
|
||||||
|
DATABASE_SAVE_DATA_LABELS=true
|
||||||
|
DATABASE_SAVE_IS_ON_WHATSAPP=true
|
||||||
|
DATABASE_SAVE_IS_ON_WHATSAPP_DAYS=7
|
||||||
|
DATABASE_DELETE_MESSAGE=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# REDIS
|
||||||
|
# ===========================================
|
||||||
|
CACHE_REDIS_ENABLED=true
|
||||||
|
CACHE_REDIS_URI=redis://localhost:6379
|
||||||
|
CACHE_REDIS_PREFIX_KEY=evolution-cache
|
||||||
|
CACHE_REDIS_TTL=604800
|
||||||
|
CACHE_REDIS_SAVE_INSTANCES=true
|
||||||
|
|
||||||
|
# Cache local (fallback)
|
||||||
|
CACHE_LOCAL_ENABLED=true
|
||||||
|
CACHE_LOCAL_TTL=86400
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# AUTENTICAÇÃO
|
||||||
|
# ===========================================
|
||||||
|
AUTHENTICATION_API_KEY=BQYHJGJHJ
|
||||||
|
AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# LOGS
|
||||||
|
# ===========================================
|
||||||
|
LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS,WEBSOCKET
|
||||||
|
LOG_COLOR=true
|
||||||
|
LOG_BAILEYS=error
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# INSTÂNCIAS
|
||||||
|
# ===========================================
|
||||||
|
DEL_INSTANCE=false
|
||||||
|
DEL_TEMP_INSTANCES=true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# IDIOMA
|
||||||
|
# ===========================================
|
||||||
|
LANGUAGE=pt-BR
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# WEBHOOK
|
||||||
|
# ===========================================
|
||||||
|
WEBHOOK_GLOBAL_URL=
|
||||||
|
WEBHOOK_GLOBAL_ENABLED=false
|
||||||
|
WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
|
||||||
|
|
||||||
|
# Eventos de webhook
|
||||||
|
WEBHOOK_EVENTS_APPLICATION_STARTUP=false
|
||||||
|
WEBHOOK_EVENTS_INSTANCE_CREATE=false
|
||||||
|
WEBHOOK_EVENTS_INSTANCE_DELETE=false
|
||||||
|
WEBHOOK_EVENTS_QRCODE_UPDATED=false
|
||||||
|
WEBHOOK_EVENTS_MESSAGES_SET=false
|
||||||
|
WEBHOOK_EVENTS_MESSAGES_UPSERT=false
|
||||||
|
WEBHOOK_EVENTS_MESSAGES_EDITED=false
|
||||||
|
WEBHOOK_EVENTS_MESSAGES_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_MESSAGES_DELETE=false
|
||||||
|
WEBHOOK_EVENTS_SEND_MESSAGE=false
|
||||||
|
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_CONTACTS_SET=false
|
||||||
|
WEBHOOK_EVENTS_CONTACTS_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_CONTACTS_UPSERT=false
|
||||||
|
WEBHOOK_EVENTS_PRESENCE_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_CHATS_SET=false
|
||||||
|
WEBHOOK_EVENTS_CHATS_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_CHATS_UPSERT=false
|
||||||
|
WEBHOOK_EVENTS_CHATS_DELETE=false
|
||||||
|
WEBHOOK_EVENTS_CONNECTION_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_LABELS_EDIT=false
|
||||||
|
WEBHOOK_EVENTS_LABELS_ASSOCIATION=false
|
||||||
|
WEBHOOK_EVENTS_GROUPS_UPSERT=false
|
||||||
|
WEBHOOK_EVENTS_GROUPS_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=false
|
||||||
|
WEBHOOK_EVENTS_CALL=false
|
||||||
|
WEBHOOK_EVENTS_TYPEBOT_START=false
|
||||||
|
WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||||
|
WEBHOOK_EVENTS_ERRORS=false
|
||||||
|
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||||
|
|
||||||
|
# Configurações de webhook
|
||||||
|
WEBHOOK_REQUEST_TIMEOUT_MS=30000
|
||||||
|
WEBHOOK_RETRY_MAX_ATTEMPTS=10
|
||||||
|
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
|
||||||
|
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
|
||||||
|
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
|
||||||
|
WEBHOOK_RETRY_JITTER_FACTOR=0.2
|
||||||
|
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# WEBSOCKET
|
||||||
|
# ===========================================
|
||||||
|
WEBSOCKET_ENABLED=true
|
||||||
|
WEBSOCKET_GLOBAL_EVENTS=true
|
||||||
|
WEBSOCKET_ALLOWED_HOSTS=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# RABBITMQ
|
||||||
|
# ===========================================
|
||||||
|
RABBITMQ_ENABLED=false
|
||||||
|
RABBITMQ_GLOBAL_ENABLED=false
|
||||||
|
RABBITMQ_PREFIX_KEY=
|
||||||
|
RABBITMQ_EXCHANGE_NAME=evolution_exchange
|
||||||
|
RABBITMQ_URI=
|
||||||
|
RABBITMQ_FRAME_MAX=8192
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# NATS
|
||||||
|
# ===========================================
|
||||||
|
NATS_ENABLED=false
|
||||||
|
NATS_GLOBAL_ENABLED=false
|
||||||
|
NATS_PREFIX_KEY=
|
||||||
|
NATS_EXCHANGE_NAME=evolution_exchange
|
||||||
|
NATS_URI=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SQS
|
||||||
|
# ===========================================
|
||||||
|
SQS_ENABLED=false
|
||||||
|
SQS_GLOBAL_ENABLED=false
|
||||||
|
SQS_GLOBAL_FORCE_SINGLE_QUEUE=false
|
||||||
|
SQS_GLOBAL_PREFIX_NAME=global
|
||||||
|
SQS_ACCESS_KEY_ID=
|
||||||
|
SQS_SECRET_ACCESS_KEY=
|
||||||
|
SQS_ACCOUNT_ID=
|
||||||
|
SQS_REGION=
|
||||||
|
SQS_MAX_PAYLOAD_SIZE=1048576
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# PUSHER
|
||||||
|
# ===========================================
|
||||||
|
PUSHER_ENABLED=false
|
||||||
|
PUSHER_GLOBAL_ENABLED=false
|
||||||
|
PUSHER_GLOBAL_APP_ID=
|
||||||
|
PUSHER_GLOBAL_KEY=
|
||||||
|
PUSHER_GLOBAL_SECRET=
|
||||||
|
PUSHER_GLOBAL_CLUSTER=
|
||||||
|
PUSHER_GLOBAL_USE_TLS=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# WHATSAPP BUSINESS
|
||||||
|
# ===========================================
|
||||||
|
WA_BUSINESS_TOKEN_WEBHOOK=evolution
|
||||||
|
WA_BUSINESS_URL=https://graph.facebook.com
|
||||||
|
WA_BUSINESS_VERSION=v18.0
|
||||||
|
WA_BUSINESS_LANGUAGE=en
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# CONFIGURAÇÕES DE SESSÃO
|
||||||
|
# ===========================================
|
||||||
|
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
||||||
|
CONFIG_SESSION_PHONE_NAME=Chrome
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# QR CODE
|
||||||
|
# ===========================================
|
||||||
|
QRCODE_LIMIT=30
|
||||||
|
QRCODE_COLOR=#198754
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# INTEGRAÇÕES
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Typebot
|
||||||
|
TYPEBOT_ENABLED=false
|
||||||
|
TYPEBOT_API_VERSION=old
|
||||||
|
TYPEBOT_SEND_MEDIA_BASE64=false
|
||||||
|
|
||||||
|
# Chatwoot
|
||||||
|
CHATWOOT_ENABLED=false
|
||||||
|
CHATWOOT_MESSAGE_DELETE=false
|
||||||
|
CHATWOOT_MESSAGE_READ=false
|
||||||
|
CHATWOOT_BOT_CONTACT=true
|
||||||
|
CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=
|
||||||
|
CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=false
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_ENABLED=false
|
||||||
|
OPENAI_API_KEY_GLOBAL=
|
||||||
|
|
||||||
|
# Dify
|
||||||
|
DIFY_ENABLED=false
|
||||||
|
|
||||||
|
# N8N
|
||||||
|
N8N_ENABLED=false
|
||||||
|
|
||||||
|
# EvoAI
|
||||||
|
EVOAI_ENABLED=false
|
||||||
|
|
||||||
|
# Flowise
|
||||||
|
FLOWISE_ENABLED=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# S3 / MINIO
|
||||||
|
# ===========================================
|
||||||
|
S3_ENABLED=false
|
||||||
|
S3_ACCESS_KEY=
|
||||||
|
S3_SECRET_KEY=
|
||||||
|
S3_ENDPOINT=
|
||||||
|
S3_BUCKET=
|
||||||
|
S3_PORT=9000
|
||||||
|
S3_USE_SSL=false
|
||||||
|
S3_REGION=
|
||||||
|
S3_SKIP_POLICY=false
|
||||||
|
S3_SAVE_VIDEO=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# MÉTRICAS
|
||||||
|
# ===========================================
|
||||||
|
PROMETHEUS_METRICS=false
|
||||||
|
METRICS_AUTH_REQUIRED=false
|
||||||
|
METRICS_USER=
|
||||||
|
METRICS_PASSWORD=
|
||||||
|
METRICS_ALLOWED_IPS=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# TELEMETRIA
|
||||||
|
# ===========================================
|
||||||
|
TELEMETRY_ENABLED=true
|
||||||
|
TELEMETRY_URL=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# PROXY
|
||||||
|
# ===========================================
|
||||||
|
PROXY_HOST=
|
||||||
|
PROXY_PORT=
|
||||||
|
PROXY_PROTOCOL=
|
||||||
|
PROXY_USERNAME=
|
||||||
|
PROXY_PASSWORD=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# CONVERSOR DE ÁUDIO
|
||||||
|
# ===========================================
|
||||||
|
API_AUDIO_CONVERTER=
|
||||||
|
API_AUDIO_CONVERTER_KEY=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# FACEBOOK
|
||||||
|
# ===========================================
|
||||||
|
FACEBOOK_APP_ID=
|
||||||
|
FACEBOOK_CONFIG_ID=
|
||||||
|
FACEBOOK_USER_TOKEN=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SENTRY
|
||||||
|
# ===========================================
|
||||||
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# EVENT EMITTER
|
||||||
|
# ===========================================
|
||||||
|
EVENT_EMITTER_MAX_LISTENERS=50
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# PROVIDER
|
||||||
|
# ===========================================
|
||||||
|
PROVIDER_ENABLED=false
|
||||||
|
PROVIDER_HOST=
|
||||||
|
PROVIDER_PORT=5656
|
||||||
|
PROVIDER_PREFIX=evolution
|
||||||
1
evolution-manager-v2
Submodule
1
evolution-manager-v2
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f054b9bc28083152d4948f835e3346fd0add39db
|
||||||
BIN
manager/dist/assets/images/evolution-logo.png
vendored
BIN
manager/dist/assets/images/evolution-logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 25 KiB |
485
manager/dist/assets/index-CO3NSIFj.js
vendored
Normal file
485
manager/dist/assets/index-CO3NSIFj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-CXH2BdD4.css
vendored
1
manager/dist/assets/index-CXH2BdD4.css
vendored
File diff suppressed because one or more lines are too long
381
manager/dist/assets/index-D-oOjDYe.js
vendored
381
manager/dist/assets/index-D-oOjDYe.js
vendored
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DsIrum0U.css
vendored
Normal file
1
manager/dist/assets/index-DsIrum0U.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
manager/dist/index.html
vendored
4
manager/dist/index.html
vendored
@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Evolution Manager</title>
|
<title>Evolution Manager</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
|
<script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
8
manager_install.sh
Executable file
8
manager_install.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
cd evolution-manager-v2
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
rm -rf manager/dist
|
||||||
|
cp -r evolution-manager-v2/dist manager/dist
|
||||||
5315
package-lock.json
generated
5315
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.3.3",
|
"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",
|
||||||
@ -56,7 +56,7 @@
|
|||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
"src/**/*.ts": [
|
"src/**/*.ts": [
|
||||||
"sh -c 'npm run build'"
|
"sh -c 'tsc --noEmit'"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -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.3",
|
"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,11 +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",
|
||||||
|
"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",
|
||||||
@ -120,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,231 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Evoai` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Evoai` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `EvoaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `EvoaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to drop the column `lid` on the `IsOnWhatsapp` table. All the data in the column will be lost.
|
||||||
|
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `N8n` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `N8n` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `N8nSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `N8nSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Nats` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Nats` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to drop the column `splitMessages` on the `Typebot` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `timePerChar` on the `Typebot` table. All the data in the column will be lost.
|
||||||
|
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to drop the column `splitMessages` on the `TypebotSetting` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `timePerChar` on the `TypebotSetting` table. All the data in the column will be lost.
|
||||||
|
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX `unique_remote_instance` ON `Chat`;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Evoai` MODIFY `triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `EvoaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `IsOnWhatsapp` DROP COLUMN `lid`,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `N8n` MODIFY `triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `N8nSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Nats` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Setting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Typebot` DROP COLUMN `splitMessages`,
|
||||||
|
DROP COLUMN `timePerChar`,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `TypebotSetting` DROP COLUMN `splitMessages`,
|
||||||
|
DROP COLUMN `timePerChar`,
|
||||||
|
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Kafka` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`enabled` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`events` JSON NOT NULL,
|
||||||
|
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updatedAt` TIMESTAMP NOT NULL,
|
||||||
|
`instanceId` VARCHAR(191) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `Kafka_instanceId_key`(`instanceId`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Kafka` ADD CONSTRAINT `Kafka_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -88,6 +88,7 @@ model Instance {
|
|||||||
Rabbitmq Rabbitmq?
|
Rabbitmq Rabbitmq?
|
||||||
Nats Nats?
|
Nats Nats?
|
||||||
Sqs Sqs?
|
Sqs Sqs?
|
||||||
|
Kafka Kafka?
|
||||||
Websocket Websocket?
|
Websocket Websocket?
|
||||||
Typebot Typebot[]
|
Typebot Typebot[]
|
||||||
Session Session?
|
Session Session?
|
||||||
@ -105,8 +106,11 @@ model Instance {
|
|||||||
EvolutionBotSetting EvolutionBotSetting?
|
EvolutionBotSetting EvolutionBotSetting?
|
||||||
Flowise Flowise[]
|
Flowise Flowise[]
|
||||||
FlowiseSetting FlowiseSetting?
|
FlowiseSetting FlowiseSetting?
|
||||||
Pusher Pusher?
|
|
||||||
N8n N8n[]
|
N8n N8n[]
|
||||||
|
N8nSetting N8nSetting?
|
||||||
|
Evoai Evoai[]
|
||||||
|
EvoaiSetting EvoaiSetting?
|
||||||
|
Pusher Pusher?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
@ -309,6 +313,16 @@ model Sqs {
|
|||||||
instanceId String @unique
|
instanceId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Kafka {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
enabled Boolean @default(false)
|
||||||
|
events Json @db.Json
|
||||||
|
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||||
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
instanceId String @unique
|
||||||
|
}
|
||||||
|
|
||||||
model Websocket {
|
model Websocket {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(false)
|
enabled Boolean @default(false)
|
||||||
@ -647,7 +661,7 @@ model IsOnWhatsapp {
|
|||||||
|
|
||||||
model N8n {
|
model N8n {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(true) @db.TinyInt(1)
|
enabled Boolean @default(true) @db.TinyInt()
|
||||||
description String? @db.VarChar(255)
|
description String? @db.VarChar(255)
|
||||||
webhookUrl String? @db.VarChar(255)
|
webhookUrl String? @db.VarChar(255)
|
||||||
basicAuthUser String? @db.VarChar(255)
|
basicAuthUser String? @db.VarChar(255)
|
||||||
@ -666,7 +680,7 @@ model N8n {
|
|||||||
triggerType TriggerType?
|
triggerType TriggerType?
|
||||||
triggerOperator TriggerOperator?
|
triggerOperator TriggerOperator?
|
||||||
triggerValue String?
|
triggerValue String?
|
||||||
createdAt DateTime? @default(now()) @db.Timestamp
|
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||||
updatedAt DateTime @updatedAt @db.Timestamp
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
instanceId String
|
instanceId String
|
||||||
@ -686,7 +700,7 @@ model N8nSetting {
|
|||||||
ignoreJids Json?
|
ignoreJids Json?
|
||||||
splitMessages Boolean? @default(false)
|
splitMessages Boolean? @default(false)
|
||||||
timePerChar Int? @default(50) @db.Int
|
timePerChar Int? @default(50) @db.Int
|
||||||
createdAt DateTime? @default(now()) @db.Timestamp
|
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||||
updatedAt DateTime @updatedAt @db.Timestamp
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||||
n8nIdFallback String? @db.VarChar(100)
|
n8nIdFallback String? @db.VarChar(100)
|
||||||
@ -696,7 +710,7 @@ model N8nSetting {
|
|||||||
|
|
||||||
model Evoai {
|
model Evoai {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(true) @db.TinyInt(1)
|
enabled Boolean @default(true) @db.TinyInt()
|
||||||
description String? @db.VarChar(255)
|
description String? @db.VarChar(255)
|
||||||
agentUrl String? @db.VarChar(255)
|
agentUrl String? @db.VarChar(255)
|
||||||
apiKey String? @db.VarChar(255)
|
apiKey String? @db.VarChar(255)
|
||||||
@ -714,7 +728,7 @@ model Evoai {
|
|||||||
triggerType TriggerType?
|
triggerType TriggerType?
|
||||||
triggerOperator TriggerOperator?
|
triggerOperator TriggerOperator?
|
||||||
triggerValue String?
|
triggerValue String?
|
||||||
createdAt DateTime? @default(now()) @db.Timestamp
|
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||||
updatedAt DateTime @updatedAt @db.Timestamp
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
instanceId String
|
instanceId String
|
||||||
@ -734,7 +748,7 @@ model EvoaiSetting {
|
|||||||
ignoreJids Json?
|
ignoreJids Json?
|
||||||
splitMessages Boolean? @default(false)
|
splitMessages Boolean? @default(false)
|
||||||
timePerChar Int? @default(50) @db.Int
|
timePerChar Int? @default(50) @db.Int
|
||||||
createdAt DateTime? @default(now()) @db.Timestamp
|
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||||
updatedAt DateTime @updatedAt @db.Timestamp
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||||
evoaiIdFallback String? @db.VarChar(100)
|
evoaiIdFallback String? @db.VarChar(100)
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Kafka" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"events" JSONB NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP NOT NULL,
|
||||||
|
"instanceId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Kafka_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "Kafka"("instanceId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -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");
|
||||||
@ -88,6 +88,7 @@ model Instance {
|
|||||||
Rabbitmq Rabbitmq?
|
Rabbitmq Rabbitmq?
|
||||||
Nats Nats?
|
Nats Nats?
|
||||||
Sqs Sqs?
|
Sqs Sqs?
|
||||||
|
Kafka Kafka?
|
||||||
Websocket Websocket?
|
Websocket Websocket?
|
||||||
Typebot Typebot[]
|
Typebot Typebot[]
|
||||||
Session Session?
|
Session Session?
|
||||||
@ -131,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])
|
||||||
}
|
}
|
||||||
@ -312,6 +314,16 @@ model Sqs {
|
|||||||
instanceId String @unique
|
instanceId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Kafka {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
enabled Boolean @default(false) @db.Boolean
|
||||||
|
events Json @db.JsonB
|
||||||
|
createdAt DateTime? @default(now()) @db.Timestamp
|
||||||
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
instanceId String @unique
|
||||||
|
}
|
||||||
|
|
||||||
model Websocket {
|
model Websocket {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(false) @db.Boolean
|
enabled Boolean @default(false) @db.Boolean
|
||||||
|
|||||||
@ -89,6 +89,7 @@ model Instance {
|
|||||||
Rabbitmq Rabbitmq?
|
Rabbitmq Rabbitmq?
|
||||||
Nats Nats?
|
Nats Nats?
|
||||||
Sqs Sqs?
|
Sqs Sqs?
|
||||||
|
Kafka Kafka?
|
||||||
Websocket Websocket?
|
Websocket Websocket?
|
||||||
Typebot Typebot[]
|
Typebot Typebot[]
|
||||||
Session Session?
|
Session Session?
|
||||||
@ -313,6 +314,16 @@ model Sqs {
|
|||||||
instanceId String @unique
|
instanceId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Kafka {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
enabled Boolean @default(false) @db.Boolean
|
||||||
|
events Json @db.JsonB
|
||||||
|
createdAt DateTime? @default(now()) @db.Timestamp
|
||||||
|
updatedAt DateTime @updatedAt @db.Timestamp
|
||||||
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
|
instanceId String @unique
|
||||||
|
}
|
||||||
|
|
||||||
model Websocket {
|
model Websocket {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
enabled Boolean @default(false) @db.Boolean
|
enabled Boolean @default(false) @db.Boolean
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { Events, wa } from '@api/types/wa.types';
|
|||||||
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
||||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||||
import { createJid } from '@utils/createJid';
|
import { createJid } from '@utils/createJid';
|
||||||
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { isBase64, isURL } from 'class-validator';
|
import { isBase64, isURL } from 'class-validator';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
@ -171,6 +172,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.log(messageRaw);
|
||||||
|
|
||||||
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
@ -323,8 +326,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
messageRaw = {
|
messageRaw = {
|
||||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||||
message: {
|
message: {
|
||||||
base64: isBase64(message.media) ? message.media : undefined,
|
base64: isBase64(message.media) ? message.media : null,
|
||||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
mediaUrl: isURL(message.media) ? message.media : null,
|
||||||
quoted,
|
quoted,
|
||||||
},
|
},
|
||||||
messageType: 'imageMessage',
|
messageType: 'imageMessage',
|
||||||
@ -337,8 +340,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
messageRaw = {
|
messageRaw = {
|
||||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||||
message: {
|
message: {
|
||||||
base64: isBase64(message.media) ? message.media : undefined,
|
base64: isBase64(message.media) ? message.media : null,
|
||||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
mediaUrl: isURL(message.media) ? message.media : null,
|
||||||
quoted,
|
quoted,
|
||||||
},
|
},
|
||||||
messageType: 'videoMessage',
|
messageType: 'videoMessage',
|
||||||
@ -351,8 +354,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
messageRaw = {
|
messageRaw = {
|
||||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||||
message: {
|
message: {
|
||||||
base64: isBase64(message.media) ? message.media : undefined,
|
base64: isBase64(message.media) ? message.media : null,
|
||||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
mediaUrl: isURL(message.media) ? message.media : null,
|
||||||
quoted,
|
quoted,
|
||||||
},
|
},
|
||||||
messageType: 'audioMessage',
|
messageType: 'audioMessage',
|
||||||
@ -372,8 +375,8 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
messageRaw = {
|
messageRaw = {
|
||||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||||
message: {
|
message: {
|
||||||
base64: isBase64(message.media) ? message.media : undefined,
|
base64: isBase64(message.media) ? message.media : null,
|
||||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
mediaUrl: isURL(message.media) ? message.media : null,
|
||||||
quoted,
|
quoted,
|
||||||
},
|
},
|
||||||
messageType: 'documentMessage',
|
messageType: 'documentMessage',
|
||||||
@ -449,7 +452,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const base64 = messageRaw.message.base64;
|
const { base64 } = messageRaw.message;
|
||||||
delete messageRaw.message.base64;
|
delete messageRaw.message.base64;
|
||||||
|
|
||||||
if (base64 || file || audioFile) {
|
if (base64 || file || audioFile) {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusine
|
|||||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||||
import { createJid } from '@utils/createJid';
|
import { createJid } from '@utils/createJid';
|
||||||
import { status } from '@utils/renderStatus';
|
import { status } from '@utils/renderStatus';
|
||||||
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { arrayUnique, isURL } from 'class-validator';
|
import { arrayUnique, isURL } from 'class-validator';
|
||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
@ -515,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') {
|
||||||
@ -553,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,
|
||||||
@ -573,7 +584,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
openAiDefaultSettings.OpenaiCreds,
|
openAiDefaultSettings.OpenaiCreds,
|
||||||
{
|
{
|
||||||
message: {
|
message: {
|
||||||
base64: messageRaw.message.base64,
|
base64: openAiBase64,
|
||||||
...messageRaw,
|
...messageRaw,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -655,6 +666,8 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.logger.log(messageRaw);
|
this.logger.log(messageRaw);
|
||||||
|
|
||||||
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
@ -1013,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'] }),
|
||||||
@ -1603,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 }) => {
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export const useVoiceCallsBaileys = async (
|
|||||||
|
|
||||||
socket.on('assertSessions', async (jids, force, callback) => {
|
socket.on('assertSessions', async (jids, force, callback) => {
|
||||||
try {
|
try {
|
||||||
const response = await baileys_sock.assertSessions(jids, force);
|
const response = await baileys_sock.assertSessions(jids);
|
||||||
|
|
||||||
callback(response);
|
callback(response);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -49,7 +49,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
try {
|
try {
|
||||||
JSON.parse(str);
|
JSON.parse(str);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,6 +180,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
remoteJid: string,
|
remoteJid: string,
|
||||||
message: string,
|
message: string,
|
||||||
settings: SettingsType,
|
settings: SettingsType,
|
||||||
|
linkPreview: boolean = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
|
||||||
@ -202,7 +203,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
if (mediaType) {
|
if (mediaType) {
|
||||||
// Send accumulated text before sending media
|
// Send accumulated text before sending media
|
||||||
if (textBuffer.trim()) {
|
if (textBuffer.trim()) {
|
||||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages, linkPreview);
|
||||||
textBuffer = '';
|
textBuffer = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,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,
|
||||||
@ -218,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,
|
||||||
@ -252,7 +253,56 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
|
|
||||||
// Send any remaining text
|
// Send any remaining text
|
||||||
if (textBuffer.trim()) {
|
if (textBuffer.trim()) {
|
||||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages, linkPreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split message by double line breaks and return array of message parts
|
||||||
|
*/
|
||||||
|
private splitMessageByDoubleLineBreaks(message: string): string[] {
|
||||||
|
return message.split('\n\n').filter((part) => part.trim().length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a single message with proper typing indicators and delays
|
||||||
|
*/
|
||||||
|
private async sendSingleMessage(
|
||||||
|
instance: any,
|
||||||
|
remoteJid: string,
|
||||||
|
message: string,
|
||||||
|
settings: any,
|
||||||
|
linkPreview: boolean = true,
|
||||||
|
): Promise<void> {
|
||||||
|
const timePerChar = settings?.timePerChar ?? 0;
|
||||||
|
const minDelay = 1000;
|
||||||
|
const maxDelay = 20000;
|
||||||
|
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||||
|
|
||||||
|
this.logger.debug(`[BaseChatbot] Sending single message with linkPreview: ${linkPreview}`);
|
||||||
|
|
||||||
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
|
await instance.client.presenceSubscribe(remoteJid);
|
||||||
|
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await instance.textMessage(
|
||||||
|
{
|
||||||
|
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
|
||||||
|
delay: settings?.delayMessage || 1000,
|
||||||
|
text: message,
|
||||||
|
linkPreview,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
|
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,67 +315,24 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
|||||||
text: string,
|
text: string,
|
||||||
settings: any,
|
settings: any,
|
||||||
splitMessages: boolean,
|
splitMessages: boolean,
|
||||||
|
linkPreview: boolean = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const timePerChar = settings?.timePerChar ?? 0;
|
|
||||||
const minDelay = 1000;
|
|
||||||
const maxDelay = 20000;
|
|
||||||
|
|
||||||
if (splitMessages) {
|
if (splitMessages) {
|
||||||
const multipleMessages = text.split('\n\n');
|
const messageParts = this.splitMessageByDoubleLineBreaks(text);
|
||||||
for (let index = 0; index < multipleMessages.length; index++) {
|
|
||||||
const message = multipleMessages[index];
|
|
||||||
if (!message.trim()) continue;
|
|
||||||
|
|
||||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
this.logger.debug(`[BaseChatbot] Splitting message into ${messageParts.length} parts`);
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
for (let index = 0; index < messageParts.length; index++) {
|
||||||
await instance.client.presenceSubscribe(remoteJid);
|
const message = messageParts[index];
|
||||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
|
||||||
|
this.logger.debug(`[BaseChatbot] Sending message part ${index + 1}/${messageParts.length}`);
|
||||||
|
await this.sendSingleMessage(instance, remoteJid, message, settings, linkPreview);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
this.logger.debug(`[BaseChatbot] All message parts sent successfully`);
|
||||||
setTimeout(async () => {
|
|
||||||
await instance.textMessage(
|
|
||||||
{
|
|
||||||
number: remoteJid.split('@')[0],
|
|
||||||
delay: settings?.delayMessage || 1000,
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
resolve();
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
|
||||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
|
this.logger.debug(`[BaseChatbot] Sending single message`);
|
||||||
|
await this.sendSingleMessage(instance, remoteJid, text, settings, linkPreview);
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
|
||||||
await instance.client.presenceSubscribe(remoteJid);
|
|
||||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
setTimeout(async () => {
|
|
||||||
await instance.textMessage(
|
|
||||||
{
|
|
||||||
number: remoteJid.split('@')[0],
|
|
||||||
delay: settings?.delayMessage || 1000,
|
|
||||||
text: text,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
resolve();
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
|
||||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,19 +91,19 @@ export class ChatbotController {
|
|||||||
pushName,
|
pushName,
|
||||||
isIntegration,
|
isIntegration,
|
||||||
};
|
};
|
||||||
await evolutionBotController.emit(emitData);
|
evolutionBotController.emit(emitData);
|
||||||
|
|
||||||
await typebotController.emit(emitData);
|
typebotController.emit(emitData);
|
||||||
|
|
||||||
await openaiController.emit(emitData);
|
openaiController.emit(emitData);
|
||||||
|
|
||||||
await difyController.emit(emitData);
|
difyController.emit(emitData);
|
||||||
|
|
||||||
await n8nController.emit(emitData);
|
n8nController.emit(emitData);
|
||||||
|
|
||||||
await evoaiController.emit(emitData);
|
evoaiController.emit(emitData);
|
||||||
|
|
||||||
await flowiseController.emit(emitData);
|
flowiseController.emit(emitData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public processDebounce(
|
public processDebounce(
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
||||||
import { ExtendedMessageKey } from '@api/integrations/channel/whatsapp/whatsapp.baileys.service';
|
|
||||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||||
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||||||
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
||||||
@ -24,10 +23,11 @@ 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 } 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';
|
||||||
@ -44,6 +44,9 @@ interface ChatwootMessage {
|
|||||||
export class ChatwootService {
|
export class ChatwootService {
|
||||||
private readonly logger = new Logger('ChatwootService');
|
private readonly logger = new Logger('ChatwootService');
|
||||||
|
|
||||||
|
// Lock polling delay
|
||||||
|
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||||
|
|
||||||
private provider: any;
|
private provider: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -130,7 +133,7 @@ export class ChatwootService {
|
|||||||
public async find(instance: InstanceDto): Promise<ChatwootDto> {
|
public async find(instance: InstanceDto): Promise<ChatwootDto> {
|
||||||
try {
|
try {
|
||||||
return await this.waMonitor.waInstances[instance.instanceName].findChatwoot();
|
return await this.waMonitor.waInstances[instance.instanceName].findChatwoot();
|
||||||
} catch (error) {
|
} catch {
|
||||||
this.logger.error('chatwoot not found');
|
this.logger.error('chatwoot not found');
|
||||||
return { enabled: null, url: '' };
|
return { enabled: null, url: '' };
|
||||||
}
|
}
|
||||||
@ -343,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;
|
||||||
@ -370,7 +383,7 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return contact;
|
return contact;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -407,11 +420,60 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
@ -568,27 +630,31 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createConversation(instance: InstanceDto, body: any) {
|
public async createConversation(instance: InstanceDto, body: any) {
|
||||||
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
|
const isLid = body.key.addressingMode === 'lid';
|
||||||
const remoteJid = 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 secounds
|
const maxWaitTime = 5000; // 5 seconds
|
||||||
|
const client = await this.clientCw(instance);
|
||||||
|
if (!client) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Processa atualização de contatos já criados @lid
|
// Processa atualização de contatos já criados @lid
|
||||||
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
|
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.senderPn) {
|
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.senderPn: ${body.key.senderPn}`,
|
`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.senderPn,
|
identifier: phoneNumber,
|
||||||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateContact === null) {
|
if (updateContact === null) {
|
||||||
const baseContact = await this.findContact(instance, body.key.senderPn.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(
|
||||||
@ -604,7 +670,25 @@ 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: any;
|
||||||
|
try {
|
||||||
|
conversationExists = await client.conversations.get({
|
||||||
|
accountId: this.provider.accountId,
|
||||||
|
conversationId: conversationId,
|
||||||
|
});
|
||||||
|
this.logger.verbose(
|
||||||
|
`Conversation exists: ID: ${conversationExists.id} - Name: ${conversationExists.meta.sender.name} - Identifier: ${conversationExists.meta.sender.identifier}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error getting conversation: ${error}`);
|
||||||
|
conversationExists = false;
|
||||||
|
}
|
||||||
|
if (!conversationExists) {
|
||||||
|
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
||||||
|
this.cache.delete(cacheKey);
|
||||||
|
return await this.createConversation(instance, body);
|
||||||
|
}
|
||||||
return conversationId;
|
return conversationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +701,7 @@ export class ChatwootService {
|
|||||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await new Promise((res) => setTimeout(res, 300));
|
await new Promise((res) => setTimeout(res, this.LOCK_POLLING_DELAY_MS));
|
||||||
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(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||||
@ -639,11 +723,7 @@ export class ChatwootService {
|
|||||||
return (await this.cache.get(cacheKey)) as number;
|
return (await this.cache.get(cacheKey)) as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await this.clientCw(instance);
|
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].split(':')[0];
|
||||||
if (!client) return null;
|
|
||||||
|
|
||||||
const isGroup = remoteJid.includes('@g.us');
|
|
||||||
const chatId = isGroup ? remoteJid : remoteJid.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;
|
||||||
@ -651,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,
|
||||||
@ -673,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -686,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) {
|
||||||
@ -721,7 +798,7 @@ export class ChatwootService {
|
|||||||
isGroup,
|
isGroup,
|
||||||
nameContact,
|
nameContact,
|
||||||
picture_url.profilePictureUrl || null,
|
picture_url.profilePictureUrl || null,
|
||||||
remoteJid,
|
phoneNumber,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,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`);
|
||||||
@ -749,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,
|
||||||
@ -769,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);
|
this.cache.set(cacheKey, inboxConversation.id, 1800);
|
||||||
return inboxConversation.id;
|
return inboxConversation.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -783,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,
|
||||||
@ -802,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);
|
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,7 +1228,7 @@ export class ChatwootService {
|
|||||||
const data: SendAudioDto = {
|
const data: SendAudioDto = {
|
||||||
number: number,
|
number: number,
|
||||||
audio: media,
|
audio: media,
|
||||||
delay: 1200,
|
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1169,7 +1239,7 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||||
type = 'document';
|
type = 'document';
|
||||||
}
|
}
|
||||||
@ -1194,6 +1264,7 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
|
throw error; // Re-throw para que o erro seja tratado pelo caller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1275,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({
|
||||||
@ -1285,7 +1357,7 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
||||||
|
|
||||||
@ -1417,7 +1489,6 @@ export class ChatwootService {
|
|||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
{
|
{
|
||||||
...messageSent,
|
...messageSent,
|
||||||
owner: instance.instanceName,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
@ -1432,7 +1503,7 @@ export class ChatwootService {
|
|||||||
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),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1452,7 +1523,6 @@ export class ChatwootService {
|
|||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
{
|
{
|
||||||
...messageSent,
|
...messageSent,
|
||||||
instanceId: instance.instanceId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
@ -1483,7 +1553,7 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (lastMessage && !lastMessage.chatwootIsRead) {
|
if (lastMessage && !lastMessage.chatwootIsRead) {
|
||||||
const key = lastMessage.key as ExtendedMessageKey;
|
const key = lastMessage.key as WAMessageKey;
|
||||||
|
|
||||||
waInstance?.markMessageAsRead({
|
waInstance?.markMessageAsRead({
|
||||||
readMessages: [
|
readMessages: [
|
||||||
@ -1520,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');
|
||||||
@ -1541,14 +1611,14 @@ export class ChatwootService {
|
|||||||
chatwootMessageIds: ChatwootMessage,
|
chatwootMessageIds: ChatwootMessage,
|
||||||
instance: InstanceDto,
|
instance: InstanceDto,
|
||||||
) {
|
) {
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
if (!chatwootMessageIds.messageId || !key?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
await this.prismaRepository.$executeRaw`
|
const result = await this.prismaRepository.$executeRaw`
|
||||||
UPDATE "Message"
|
UPDATE "Message"
|
||||||
SET
|
SET
|
||||||
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
||||||
@ -1560,8 +1630,14 @@ export class ChatwootService {
|
|||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1609,12 +1685,13 @@ export class ChatwootService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = message?.key as ExtendedMessageKey;
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1640,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;
|
||||||
@ -1913,6 +1994,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.upsert' || event === 'send.message') {
|
if (event === 'messages.upsert' || event === 'send.message') {
|
||||||
|
this.logger.info(`[${event}] New message received - Instance: ${JSON.stringify(body, null, 2)}`);
|
||||||
if (body.key.remoteJid === 'status@broadcast') {
|
if (body.key.remoteJid === 'status@broadcast') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1957,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;
|
||||||
}
|
}
|
||||||
@ -2003,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(
|
||||||
@ -2086,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' });
|
||||||
@ -2144,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;
|
||||||
|
|
||||||
@ -2235,9 +2354,21 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||||
const editedText = `${
|
const editedMessageContentRaw =
|
||||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
body?.editedMessage?.conversation ??
|
||||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
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) {
|
||||||
@ -2245,11 +2376,14 @@ export class ChatwootService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as WAMessageKey;
|
||||||
|
|
||||||
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
||||||
|
|
||||||
if (message && message.chatwootConversationId) {
|
if (message && message.chatwootConversationId && message.chatwootMessageId) {
|
||||||
|
// Criar nova mensagem com formato: "Mensagem editada:\n\nteste1"
|
||||||
|
const editedText = `\n\n\`${i18next.t('cw.message.edited')}:\`\n\n${editedMessageContent}`;
|
||||||
|
|
||||||
const send = await this.createMessage(
|
const send = await this.createMessage(
|
||||||
instance,
|
instance,
|
||||||
message.chatwootConversationId,
|
message.chatwootConversationId,
|
||||||
@ -2301,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,
|
||||||
});
|
});
|
||||||
@ -2327,15 +2461,30 @@ export class ChatwootService {
|
|||||||
await this.createBotMessage(instance, msgStatus, 'incoming');
|
await this.createBotMessage(instance, msgStatus, 'incoming');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'connection.update') {
|
if (event === 'connection.update' && body.status === 'open') {
|
||||||
if (body.status === 'open') {
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
// if we have qrcode count then we understand that a new connection was established
|
if (!waInstance) return;
|
||||||
if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) {
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0);
|
||||||
|
|
||||||
|
// Se a conexão foi estabelecida via QR code, notifica imediatamente.
|
||||||
|
if (waInstance.qrCode && waInstance.qrCode.count > 0) {
|
||||||
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');
|
||||||
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
|
waInstance.qrCode.count = 0;
|
||||||
|
waInstance.lastConnectionNotification = now;
|
||||||
chatwootImport.clearAll(instance);
|
chatwootImport.clearAll(instance);
|
||||||
}
|
}
|
||||||
|
// Se não foi via QR code, verifica o throttling.
|
||||||
|
else if (timeSinceLastNotification >= 30000) {
|
||||||
|
const msgConnection = i18next.t('cw.inbox.connected');
|
||||||
|
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||||||
|
waInstance.lastConnectionNotification = now;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2378,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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2552,7 +2707,7 @@ export class ChatwootService {
|
|||||||
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
|
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
|
||||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
waInstance.clearCacheChatwoot();
|
waInstance.clearCacheChatwoot();
|
||||||
} catch (error) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,12 +112,19 @@ class ChatwootImport {
|
|||||||
const bindInsert = [provider.accountId];
|
const bindInsert = [provider.accountId];
|
||||||
|
|
||||||
for (const contact of contactsChunk) {
|
for (const contact of contactsChunk) {
|
||||||
bindInsert.push(contact.pushName);
|
const isGroup = this.isIgnorePhoneNumber(contact.remoteJid);
|
||||||
|
|
||||||
|
const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName;
|
||||||
|
bindInsert.push(contactName);
|
||||||
const bindName = `$${bindInsert.length}`;
|
const bindName = `$${bindInsert.length}`;
|
||||||
|
|
||||||
|
let bindPhoneNumber: string;
|
||||||
|
if (!isGroup) {
|
||||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||||
const bindPhoneNumber = `$${bindInsert.length}`;
|
bindPhoneNumber = `$${bindInsert.length}`;
|
||||||
|
} else {
|
||||||
|
bindPhoneNumber = 'NULL';
|
||||||
|
}
|
||||||
bindInsert.push(contact.remoteJid);
|
bindInsert.push(contact.remoteJid);
|
||||||
const bindIdentifier = `$${bindInsert.length}`;
|
const bindIdentifier = `$${bindInsert.length}`;
|
||||||
|
|
||||||
@ -130,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;
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Integration } from '@api/types/wa.types';
|
|||||||
import { ConfigService, HttpServer } from '@config/env.config';
|
import { ConfigService, HttpServer } from '@config/env.config';
|
||||||
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { isURL } from 'class-validator';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
import { OpenaiService } from '../../openai/services/openai.service';
|
import { OpenaiService } from '../../openai/services/openai.service';
|
||||||
@ -78,15 +79,35 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
|
|
||||||
// Handle image messages
|
// Handle image messages
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
|
let mediaBase64 = msg.message.base64 || null;
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||||
|
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||||
|
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaBase64) {
|
||||||
payload.files = [
|
payload.files = [
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
transfer_method: 'remote_url',
|
transfer_method: 'remote_url',
|
||||||
url: contentSplit[1].split('?')[0],
|
url: mediaBase64,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.query = contentSplit[2] || content;
|
}
|
||||||
|
} else {
|
||||||
|
payload.files = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
transfer_method: 'remote_url',
|
||||||
|
url: media[1].split('?')[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
payload.query = media[2] || content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -107,7 +128,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
const conversationId = response?.data?.conversation_id;
|
const conversationId = response?.data?.conversation_id;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaRepository.integrationSession.update({
|
await this.prismaRepository.integrationSession.update({
|
||||||
@ -140,15 +161,35 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
|
|
||||||
// Handle image messages
|
// Handle image messages
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
|
let mediaBase64 = msg.message.base64 || null;
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||||
|
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||||
|
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaBase64) {
|
||||||
payload.files = [
|
payload.files = [
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
transfer_method: 'remote_url',
|
transfer_method: 'remote_url',
|
||||||
url: contentSplit[1].split('?')[0],
|
url: mediaBase64,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.inputs.query = contentSplit[2] || content;
|
}
|
||||||
|
} else {
|
||||||
|
payload.files = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
transfer_method: 'remote_url',
|
||||||
|
url: media[1].split('?')[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
payload.inputs.query = media[2] || content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -169,7 +210,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
const conversationId = response?.data?.conversation_id;
|
const conversationId = response?.data?.conversation_id;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaRepository.integrationSession.update({
|
await this.prismaRepository.integrationSession.update({
|
||||||
@ -202,15 +243,26 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
|
|
||||||
// Handle image messages
|
// Handle image messages
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
payload.files = [
|
payload.files = [
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
transfer_method: 'remote_url',
|
transfer_method: 'remote_url',
|
||||||
url: contentSplit[1].split('?')[0],
|
url: msg.message.mediaUrl || msg.message.base64,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.query = contentSplit[2] || content;
|
} else {
|
||||||
|
payload.files = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
transfer_method: 'remote_url',
|
||||||
|
url: media[1].split('?')[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
payload.query = media[2] || content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -246,7 +298,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
|||||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaRepository.integrationSession.update({
|
await this.prismaRepository.integrationSession.update({
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ConfigService, HttpServer } from '@config/env.config';
|
|||||||
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
|
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { downloadMediaMessage } from 'baileys';
|
import { downloadMediaMessage } from 'baileys';
|
||||||
|
import { isURL } from 'class-validator';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
@ -82,14 +83,33 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
|||||||
|
|
||||||
// Handle image message if present
|
// Handle image message if present
|
||||||
if (this.isImageMessage(content) && msg) {
|
if (this.isImageMessage(content) && msg) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
parts[0].text = contentSplit[2] || content;
|
parts[0].text = media[2] || content;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
|
let mediaBase64 = msg.message.base64 || null;
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||||
|
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||||
|
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaBase64) {
|
||||||
|
parts.push({
|
||||||
|
type: 'file',
|
||||||
|
file: {
|
||||||
|
name: msg.key.id + '.jpeg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
bytes: mediaBase64,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Download the image
|
// Download the image
|
||||||
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
|
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
|
||||||
const fileContent = Buffer.from(mediaBuffer).toString('base64');
|
const fileContent = Buffer.from(mediaBuffer).toString('base64');
|
||||||
const fileName = contentSplit[2] || `${msg.key?.id || 'image'}.jpg`;
|
const fileName = media[2] || `${msg.key?.id || 'image'}.jpg`;
|
||||||
|
|
||||||
parts.push({
|
parts.push({
|
||||||
type: 'file',
|
type: 'file',
|
||||||
@ -99,6 +119,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
|||||||
bytes: fileContent,
|
bytes: fileContent,
|
||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
|
}
|
||||||
} catch (fileErr) {
|
} catch (fileErr) {
|
||||||
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
|
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
|
||||||
}
|
}
|
||||||
@ -174,7 +195,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
|||||||
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
|
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ConfigService, HttpServer } from '@config/env.config';
|
|||||||
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
|
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
|
||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { isURL } from 'class-validator';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
import { OpenaiService } from '../../openai/services/openai.service';
|
import { OpenaiService } from '../../openai/services/openai.service';
|
||||||
@ -71,16 +72,26 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content) && msg) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
payload.files = [
|
payload.files = [
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
url: contentSplit[1].split('?')[0],
|
url: msg.message.base64 || msg.message.mediaUrl,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.query = contentSplit[2] || content;
|
} else {
|
||||||
|
payload.files = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
url: media[1].split('?')[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.query = media[2] || content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -115,15 +126,10 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.debug(`[EvolutionBot] Sending request to endpoint: ${endpoint}`);
|
|
||||||
this.logger.debug(`[EvolutionBot] Request payload: ${JSON.stringify(sanitizedPayload, null, 2)}`);
|
|
||||||
|
|
||||||
const response = await axios.post(endpoint, payload, {
|
const response = await axios.post(endpoint, payload, {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug(`[EvolutionBot] Response received - Status: ${response.status}`);
|
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||||
}
|
}
|
||||||
@ -134,10 +140,6 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
|
|||||||
// Validate linkPreview is boolean and default to true for backward compatibility
|
// Validate linkPreview is boolean and default to true for backward compatibility
|
||||||
const linkPreview = typeof rawLinkPreview === 'boolean' ? rawLinkPreview : true;
|
const linkPreview = typeof rawLinkPreview === 'boolean' ? rawLinkPreview : true;
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`[EvolutionBot] Processing response - Message length: ${message?.length || 0}, LinkPreview: ${linkPreview}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
|
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
|
||||||
const innerContent = message.slice(1, -1);
|
const innerContent = message.slice(1, -1);
|
||||||
if (!innerContent.includes("'")) {
|
if (!innerContent.includes("'")) {
|
||||||
@ -146,17 +148,8 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
// Send message directly with validated linkPreview option
|
// Use the base class method that handles splitMessages functionality
|
||||||
await instance.textMessage(
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, linkPreview);
|
||||||
{
|
|
||||||
number: remoteJid.split('@')[0],
|
|
||||||
delay: settings?.delayMessage || 1000,
|
|
||||||
text: message,
|
|
||||||
linkPreview, // Always boolean, defaults to true
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
this.logger.debug(`[EvolutionBot] Message sent successfully with linkPreview: ${linkPreview}`);
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`[EvolutionBot] No message content received from bot response`);
|
this.logger.warn(`[EvolutionBot] No message content received from bot response`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Integration } from '@api/types/wa.types';
|
|||||||
import { ConfigService, HttpServer } from '@config/env.config';
|
import { ConfigService, HttpServer } from '@config/env.config';
|
||||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { isURL } from 'class-validator';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
import { OpenaiService } from '../../openai/services/openai.service';
|
import { OpenaiService } from '../../openai/services/openai.service';
|
||||||
@ -82,17 +83,28 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
payload.uploads = [
|
payload.uploads = [
|
||||||
{
|
{
|
||||||
data: contentSplit[1].split('?')[0],
|
data: msg.message.base64 || msg.message.mediaUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
name: 'Flowise.png',
|
name: 'Flowise.png',
|
||||||
mime: 'image/png',
|
mime: 'image/png',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.question = contentSplit[2] || content;
|
} else {
|
||||||
|
payload.uploads = [
|
||||||
|
{
|
||||||
|
data: media[1].split('?')[0],
|
||||||
|
type: 'url',
|
||||||
|
name: 'Flowise.png',
|
||||||
|
mime: 'image/png',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
payload.question = media[2] || content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
@ -130,7 +142,7 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
|||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
// Use the base class method to send the message to WhatsApp
|
// Use the base class method to send the message to WhatsApp
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
@ -78,7 +79,7 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
|||||||
const message = response?.data?.output || response?.data?.answer;
|
const message = response?.data?.output || response?.data?.answer;
|
||||||
|
|
||||||
// Use base class method instead of custom implementation
|
// Use base class method instead of custom implementation
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
|
|
||||||
await this.prismaRepository.integrationSession.update({
|
await this.prismaRepository.integrationSession.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { IntegrationSession, OpenaiBot, OpenaiSetting } from '@prisma/client';
|
|||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { downloadMediaMessage } from 'baileys';
|
import { downloadMediaMessage } from 'baileys';
|
||||||
|
import { isURL } from 'class-validator';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import P from 'pino';
|
import P from 'pino';
|
||||||
@ -85,6 +86,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
remoteJid,
|
remoteJid,
|
||||||
"Sorry, I couldn't transcribe your audio message. Could you please type your message instead?",
|
"Sorry, I couldn't transcribe your audio message. Could you please type your message instead?",
|
||||||
settings,
|
settings,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -173,7 +175,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process with the appropriate API based on bot type
|
// Process with the appropriate API based on bot type
|
||||||
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content);
|
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content, msg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error in process: ${error.message || JSON.stringify(error)}`);
|
this.logger.error(`Error in process: ${error.message || JSON.stringify(error)}`);
|
||||||
return;
|
return;
|
||||||
@ -191,6 +193,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
remoteJid: string,
|
remoteJid: string,
|
||||||
pushName: string,
|
pushName: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
msg?: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.log(`Sending message to bot for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
|
this.logger.log(`Sending message to bot for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
|
||||||
|
|
||||||
@ -222,10 +225,11 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
pushName,
|
pushName,
|
||||||
false, // Not fromMe
|
false, // Not fromMe
|
||||||
content,
|
content,
|
||||||
|
msg,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.log('Processing with ChatCompletion API');
|
this.logger.log('Processing with ChatCompletion API');
|
||||||
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content);
|
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Got response from OpenAI: ${message?.substring(0, 50)}${message?.length > 50 ? '...' : ''}`);
|
this.logger.log(`Got response from OpenAI: ${message?.substring(0, 50)}${message?.length > 50 ? '...' : ''}`);
|
||||||
@ -233,7 +237,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
// Send the response
|
// Send the response
|
||||||
if (message) {
|
if (message) {
|
||||||
this.logger.log('Sending message to WhatsApp');
|
this.logger.log('Sending message to WhatsApp');
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||||
} else {
|
} else {
|
||||||
this.logger.error('No message to send to WhatsApp');
|
this.logger.error('No message to send to WhatsApp');
|
||||||
}
|
}
|
||||||
@ -268,6 +272,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
pushName: string,
|
pushName: string,
|
||||||
fromMe: boolean,
|
fromMe: boolean,
|
||||||
content: string,
|
content: string,
|
||||||
|
msg?: any,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const messageData: any = {
|
const messageData: any = {
|
||||||
role: fromMe ? 'assistant' : 'user',
|
role: fromMe ? 'assistant' : 'user',
|
||||||
@ -276,11 +281,27 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
|
|
||||||
// Handle image messages
|
// Handle image messages
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
const url = contentSplit[1].split('?')[0];
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
|
let mediaBase64 = msg.message.base64 || null;
|
||||||
|
|
||||||
|
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||||
|
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||||
|
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaBase64) {
|
||||||
|
messageData.content = [
|
||||||
|
{ type: 'text', text: media[2] || content },
|
||||||
|
{ type: 'image_url', image_url: { url: mediaBase64 } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const url = media[1].split('?')[0];
|
||||||
|
|
||||||
messageData.content = [
|
messageData.content = [
|
||||||
{ type: 'text', text: contentSplit[2] || content },
|
{ type: 'text', text: media[2] || content },
|
||||||
{
|
{
|
||||||
type: 'image_url',
|
type: 'image_url',
|
||||||
image_url: {
|
image_url: {
|
||||||
@ -289,6 +310,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get thread ID from session or create new thread
|
// Get thread ID from session or create new thread
|
||||||
let threadId = session.sessionId;
|
let threadId = session.sessionId;
|
||||||
@ -376,6 +398,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
openaiBot: OpenaiBot,
|
openaiBot: OpenaiBot,
|
||||||
remoteJid: string,
|
remoteJid: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
msg?: any,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
this.logger.log('Starting processChatCompletionMessage');
|
this.logger.log('Starting processChatCompletionMessage');
|
||||||
|
|
||||||
@ -468,11 +491,18 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
// Handle image messages
|
// Handle image messages
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
this.logger.log('Found image message');
|
this.logger.log('Found image message');
|
||||||
const contentSplit = content.split('|');
|
const media = content.split('|');
|
||||||
const url = contentSplit[1].split('?')[0];
|
|
||||||
|
if (msg.message.mediaUrl || msg.message.base64) {
|
||||||
|
messageData.content = [
|
||||||
|
{ type: 'text', text: media[2] || content },
|
||||||
|
{ type: 'image_url', image_url: { url: msg.message.base64 || msg.message.mediaUrl } },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const url = media[1].split('?')[0];
|
||||||
|
|
||||||
messageData.content = [
|
messageData.content = [
|
||||||
{ type: 'text', text: contentSplit[2] || content },
|
{ type: 'text', text: media[2] || content },
|
||||||
{
|
{
|
||||||
type: 'image_url',
|
type: 'image_url',
|
||||||
image_url: {
|
image_url: {
|
||||||
@ -481,6 +511,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Combine all messages: system messages, pre-defined messages, conversation history, and current message
|
// Combine all messages: system messages, pre-defined messages, conversation history, and current message
|
||||||
const messages: any[] = [
|
const messages: any[] = [
|
||||||
|
|||||||
@ -318,7 +318,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
} else if (formattedText.includes('[buttons]')) {
|
} else if (formattedText.includes('[buttons]')) {
|
||||||
await this.processButtonMessage(instance, formattedText, session.remoteJid);
|
await this.processButtonMessage(instance, formattedText, session.remoteJid);
|
||||||
} else {
|
} else {
|
||||||
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
|
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
@ -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,
|
||||||
@ -393,7 +393,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
} else if (formattedText.includes('[buttons]')) {
|
} else if (formattedText.includes('[buttons]')) {
|
||||||
await this.processButtonMessage(instance, formattedText, session.remoteJid);
|
await this.processButtonMessage(instance, formattedText, session.remoteJid);
|
||||||
} else {
|
} else {
|
||||||
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
|
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
@ -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: '',
|
||||||
@ -642,7 +642,11 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
if (unknownMessage) {
|
if (unknownMessage) {
|
||||||
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
|
await this.sendMessageWhatsApp(
|
||||||
|
waInstance,
|
||||||
|
remoteJid,
|
||||||
|
unknownMessage,
|
||||||
|
{
|
||||||
delayMessage,
|
delayMessage,
|
||||||
expire,
|
expire,
|
||||||
keywordFinish,
|
keywordFinish,
|
||||||
@ -650,7 +654,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
stopBotFromMe,
|
stopBotFromMe,
|
||||||
keepOpen,
|
keepOpen,
|
||||||
unknownMessage,
|
unknownMessage,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -801,7 +807,11 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
if (!data?.messages || data.messages.length === 0) {
|
if (!data?.messages || data.messages.length === 0) {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
if (unknownMessage) {
|
if (unknownMessage) {
|
||||||
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
|
await this.sendMessageWhatsApp(
|
||||||
|
waInstance,
|
||||||
|
remoteJid,
|
||||||
|
unknownMessage,
|
||||||
|
{
|
||||||
delayMessage,
|
delayMessage,
|
||||||
expire,
|
expire,
|
||||||
keywordFinish,
|
keywordFinish,
|
||||||
@ -809,7 +819,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
stopBotFromMe,
|
stopBotFromMe,
|
||||||
keepOpen,
|
keepOpen,
|
||||||
unknownMessage,
|
unknownMessage,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -903,7 +915,11 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
if (unknownMessage) {
|
if (unknownMessage) {
|
||||||
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
|
await this.sendMessageWhatsApp(
|
||||||
|
waInstance,
|
||||||
|
remoteJid,
|
||||||
|
unknownMessage,
|
||||||
|
{
|
||||||
delayMessage,
|
delayMessage,
|
||||||
expire,
|
expire,
|
||||||
keywordFinish,
|
keywordFinish,
|
||||||
@ -911,7 +927,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
|||||||
stopBotFromMe,
|
stopBotFromMe,
|
||||||
keepOpen,
|
keepOpen,
|
||||||
unknownMessage,
|
unknownMessage,
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
sendTelemetry('/message/sendText');
|
sendTelemetry('/message/sendText');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -40,6 +40,11 @@ export class EventDto {
|
|||||||
useTLS?: boolean;
|
useTLS?: boolean;
|
||||||
events?: string[];
|
events?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
kafka?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
events?: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
||||||
@ -82,5 +87,10 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
|||||||
useTLS?: boolean;
|
useTLS?: boolean;
|
||||||
events?: string[];
|
events?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
kafka?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
events?: string[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { KafkaController } from '@api/integrations/event/kafka/kafka.controller';
|
||||||
import { NatsController } from '@api/integrations/event/nats/nats.controller';
|
import { NatsController } from '@api/integrations/event/nats/nats.controller';
|
||||||
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
|
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
|
||||||
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
|
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
|
||||||
@ -17,6 +18,7 @@ export class EventManager {
|
|||||||
private natsController: NatsController;
|
private natsController: NatsController;
|
||||||
private sqsController: SqsController;
|
private sqsController: SqsController;
|
||||||
private pusherController: PusherController;
|
private pusherController: PusherController;
|
||||||
|
private kafkaController: KafkaController;
|
||||||
|
|
||||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||||
this.prisma = prismaRepository;
|
this.prisma = prismaRepository;
|
||||||
@ -28,6 +30,7 @@ export class EventManager {
|
|||||||
this.nats = new NatsController(prismaRepository, waMonitor);
|
this.nats = new NatsController(prismaRepository, waMonitor);
|
||||||
this.sqs = new SqsController(prismaRepository, waMonitor);
|
this.sqs = new SqsController(prismaRepository, waMonitor);
|
||||||
this.pusher = new PusherController(prismaRepository, waMonitor);
|
this.pusher = new PusherController(prismaRepository, waMonitor);
|
||||||
|
this.kafka = new KafkaController(prismaRepository, waMonitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public set prisma(prisma: PrismaRepository) {
|
public set prisma(prisma: PrismaRepository) {
|
||||||
@ -93,12 +96,20 @@ export class EventManager {
|
|||||||
return this.pusherController;
|
return this.pusherController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set kafka(kafka: KafkaController) {
|
||||||
|
this.kafkaController = kafka;
|
||||||
|
}
|
||||||
|
public get kafka() {
|
||||||
|
return this.kafkaController;
|
||||||
|
}
|
||||||
|
|
||||||
public init(httpServer: Server): void {
|
public init(httpServer: Server): void {
|
||||||
this.websocket.init(httpServer);
|
this.websocket.init(httpServer);
|
||||||
this.rabbitmq.init();
|
this.rabbitmq.init();
|
||||||
this.nats.init();
|
this.nats.init();
|
||||||
this.sqs.init();
|
this.sqs.init();
|
||||||
this.pusher.init();
|
this.pusher.init();
|
||||||
|
this.kafka.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async emit(eventData: {
|
public async emit(eventData: {
|
||||||
@ -112,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);
|
||||||
@ -119,42 +131,47 @@ export class EventManager {
|
|||||||
await this.sqs.emit(eventData);
|
await this.sqs.emit(eventData);
|
||||||
await this.webhook.emit(eventData);
|
await this.webhook.emit(eventData);
|
||||||
await this.pusher.emit(eventData);
|
await this.pusher.emit(eventData);
|
||||||
|
await this.kafka.emit(eventData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setInstance(instanceName: string, data: any): Promise<any> {
|
public async setInstance(instanceName: string, data: any): Promise<any> {
|
||||||
if (data.websocket)
|
if (data.websocket) {
|
||||||
await this.websocket.set(instanceName, {
|
await this.websocket.set(instanceName, {
|
||||||
websocket: {
|
websocket: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
events: data.websocket?.events,
|
events: data.websocket?.events,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.rabbitmq)
|
if (data.rabbitmq) {
|
||||||
await this.rabbitmq.set(instanceName, {
|
await this.rabbitmq.set(instanceName, {
|
||||||
rabbitmq: {
|
rabbitmq: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
events: data.rabbitmq?.events,
|
events: data.rabbitmq?.events,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.nats)
|
if (data.nats) {
|
||||||
await this.nats.set(instanceName, {
|
await this.nats.set(instanceName, {
|
||||||
nats: {
|
nats: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
events: data.nats?.events,
|
events: data.nats?.events,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.sqs)
|
if (data.sqs) {
|
||||||
await this.sqs.set(instanceName, {
|
await this.sqs.set(instanceName, {
|
||||||
sqs: {
|
sqs: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
events: data.sqs?.events,
|
events: data.sqs?.events,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.webhook)
|
if (data.webhook) {
|
||||||
await this.webhook.set(instanceName, {
|
await this.webhook.set(instanceName, {
|
||||||
webhook: {
|
webhook: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -165,8 +182,9 @@ export class EventManager {
|
|||||||
byEvents: data.webhook?.byEvents,
|
byEvents: data.webhook?.byEvents,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.pusher)
|
if (data.pusher) {
|
||||||
await this.pusher.set(instanceName, {
|
await this.pusher.set(instanceName, {
|
||||||
pusher: {
|
pusher: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -179,4 +197,14 @@ export class EventManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.kafka) {
|
||||||
|
await this.kafka.set(instanceName, {
|
||||||
|
kafka: {
|
||||||
|
enabled: true,
|
||||||
|
events: data.kafka?.events,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { KafkaRouter } from '@api/integrations/event/kafka/kafka.router';
|
||||||
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
|
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
|
||||||
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
|
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
|
||||||
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
|
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
|
||||||
@ -18,5 +19,6 @@ export class EventRouter {
|
|||||||
this.router.use('/nats', new NatsRouter(...guards).router);
|
this.router.use('/nats', new NatsRouter(...guards).router);
|
||||||
this.router.use('/pusher', new PusherRouter(...guards).router);
|
this.router.use('/pusher', new PusherRouter(...guards).router);
|
||||||
this.router.use('/sqs', new SqsRouter(...guards).router);
|
this.router.use('/sqs', new SqsRouter(...guards).router);
|
||||||
|
this.router.use('/kafka', new KafkaRouter(...guards).router);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ export const eventSchema: JSONSchema7 = {
|
|||||||
sqs: {
|
sqs: {
|
||||||
$ref: '#/$defs/event',
|
$ref: '#/$defs/event',
|
||||||
},
|
},
|
||||||
|
kafka: {
|
||||||
|
$ref: '#/$defs/event',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
$defs: {
|
$defs: {
|
||||||
event: {
|
event: {
|
||||||
|
|||||||
416
src/api/integrations/event/kafka/kafka.controller.ts
Normal file
416
src/api/integrations/event/kafka/kafka.controller.ts
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
import { PrismaRepository } from '@api/repository/repository.service';
|
||||||
|
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||||
|
import { configService, Kafka, Log } from '@config/env.config';
|
||||||
|
import { Logger } from '@config/logger.config';
|
||||||
|
import { Consumer, ConsumerConfig, Kafka as KafkaJS, KafkaConfig, Producer, ProducerConfig } from 'kafkajs';
|
||||||
|
|
||||||
|
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||||
|
|
||||||
|
export class KafkaController extends EventController implements EventControllerInterface {
|
||||||
|
private kafkaClient: KafkaJS | null = null;
|
||||||
|
private producer: Producer | null = null;
|
||||||
|
private consumer: Consumer | null = null;
|
||||||
|
private readonly logger = new Logger('KafkaController');
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private maxReconnectAttempts = 10;
|
||||||
|
private reconnectDelay = 5000; // 5 seconds
|
||||||
|
private isReconnecting = false;
|
||||||
|
|
||||||
|
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||||
|
super(prismaRepository, waMonitor, configService.get<Kafka>('KAFKA')?.ENABLED, 'kafka');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
if (!this.status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async connect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const kafkaConfig = configService.get<Kafka>('KAFKA');
|
||||||
|
|
||||||
|
const clientConfig: KafkaConfig = {
|
||||||
|
clientId: kafkaConfig.CLIENT_ID || 'evolution-api',
|
||||||
|
brokers: kafkaConfig.BROKERS || ['localhost:9092'],
|
||||||
|
connectionTimeout: kafkaConfig.CONNECTION_TIMEOUT || 3000,
|
||||||
|
requestTimeout: kafkaConfig.REQUEST_TIMEOUT || 30000,
|
||||||
|
retry: {
|
||||||
|
initialRetryTime: 100,
|
||||||
|
retries: 8,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add SASL authentication if configured
|
||||||
|
if (kafkaConfig.SASL?.ENABLED) {
|
||||||
|
clientConfig.sasl = {
|
||||||
|
mechanism: (kafkaConfig.SASL.MECHANISM as any) || 'plain',
|
||||||
|
username: kafkaConfig.SASL.USERNAME,
|
||||||
|
password: kafkaConfig.SASL.PASSWORD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add SSL configuration if enabled
|
||||||
|
if (kafkaConfig.SSL?.ENABLED) {
|
||||||
|
clientConfig.ssl = {
|
||||||
|
rejectUnauthorized: kafkaConfig.SSL.REJECT_UNAUTHORIZED !== false,
|
||||||
|
ca: kafkaConfig.SSL.CA ? [kafkaConfig.SSL.CA] : undefined,
|
||||||
|
key: kafkaConfig.SSL.KEY,
|
||||||
|
cert: kafkaConfig.SSL.CERT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.kafkaClient = new KafkaJS(clientConfig);
|
||||||
|
|
||||||
|
// Initialize producer
|
||||||
|
const producerConfig: ProducerConfig = {
|
||||||
|
maxInFlightRequests: 1,
|
||||||
|
idempotent: true,
|
||||||
|
transactionTimeout: 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.producer = this.kafkaClient.producer(producerConfig);
|
||||||
|
await this.producer.connect();
|
||||||
|
|
||||||
|
// Initialize consumer for global events if enabled
|
||||||
|
if (kafkaConfig.GLOBAL_ENABLED) {
|
||||||
|
await this.initGlobalConsumer();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.isReconnecting = false;
|
||||||
|
|
||||||
|
this.logger.info('Kafka initialized successfully');
|
||||||
|
|
||||||
|
// Create topics if they don't exist
|
||||||
|
if (kafkaConfig.AUTO_CREATE_TOPICS) {
|
||||||
|
await this.createTopics();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({
|
||||||
|
local: 'KafkaController.connect',
|
||||||
|
message: 'Failed to connect to Kafka',
|
||||||
|
error: error.message || error,
|
||||||
|
});
|
||||||
|
this.scheduleReconnect();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initGlobalConsumer(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const kafkaConfig = configService.get<Kafka>('KAFKA');
|
||||||
|
|
||||||
|
const consumerConfig: ConsumerConfig = {
|
||||||
|
groupId: kafkaConfig.CONSUMER_GROUP_ID || 'evolution-api-consumers',
|
||||||
|
sessionTimeout: 30000,
|
||||||
|
heartbeatInterval: 3000,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.consumer = this.kafkaClient.consumer(consumerConfig);
|
||||||
|
await this.consumer.connect();
|
||||||
|
|
||||||
|
// Subscribe to global topics
|
||||||
|
const events = kafkaConfig.EVENTS;
|
||||||
|
if (events) {
|
||||||
|
const eventKeys = Object.keys(events).filter((event) => events[event]);
|
||||||
|
|
||||||
|
for (const event of eventKeys) {
|
||||||
|
const topicName = this.getTopicName(event, true);
|
||||||
|
await this.consumer.subscribe({ topic: topicName });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start consuming messages
|
||||||
|
await this.consumer.run({
|
||||||
|
eachMessage: async ({ topic, message }) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message.value?.toString() || '{}');
|
||||||
|
this.logger.debug(`Received message from topic ${topic}: ${JSON.stringify(data)}`);
|
||||||
|
|
||||||
|
// Process the message here if needed
|
||||||
|
// This is where you can add custom message processing logic
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error processing message from topic ${topic}: ${error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info('Global Kafka consumer initialized');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to initialize global Kafka consumer: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTopics(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const kafkaConfig = configService.get<Kafka>('KAFKA');
|
||||||
|
const admin = this.kafkaClient.admin();
|
||||||
|
await admin.connect();
|
||||||
|
|
||||||
|
const topics = [];
|
||||||
|
|
||||||
|
// Create global topics if enabled
|
||||||
|
if (kafkaConfig.GLOBAL_ENABLED && kafkaConfig.EVENTS) {
|
||||||
|
const eventKeys = Object.keys(kafkaConfig.EVENTS).filter((event) => kafkaConfig.EVENTS[event]);
|
||||||
|
|
||||||
|
for (const event of eventKeys) {
|
||||||
|
const topicName = this.getTopicName(event, true);
|
||||||
|
topics.push({
|
||||||
|
topic: topicName,
|
||||||
|
numPartitions: kafkaConfig.NUM_PARTITIONS || 1,
|
||||||
|
replicationFactor: kafkaConfig.REPLICATION_FACTOR || 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topics.length > 0) {
|
||||||
|
await admin.createTopics({
|
||||||
|
topics,
|
||||||
|
waitForLeaders: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info(`Created ${topics.length} Kafka topics`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await admin.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to create Kafka topics: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTopicName(event: string, isGlobal: boolean = false, instanceName?: string): string {
|
||||||
|
const kafkaConfig = configService.get<Kafka>('KAFKA');
|
||||||
|
const prefix = kafkaConfig.TOPIC_PREFIX || 'evolution';
|
||||||
|
|
||||||
|
if (isGlobal) {
|
||||||
|
return `${prefix}.global.${event.toLowerCase().replace(/_/g, '.')}`;
|
||||||
|
} else {
|
||||||
|
return `${prefix}.${instanceName}.${event.toLowerCase().replace(/_/g, '.')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleConnectionLoss(): void {
|
||||||
|
if (this.isReconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanup();
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleReconnect(): void {
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
this.logger.error(
|
||||||
|
`Maximum reconnect attempts (${this.maxReconnectAttempts}) reached. Stopping reconnection attempts.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isReconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isReconnecting = true;
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
|
||||||
|
const delay = this.reconnectDelay * Math.pow(2, Math.min(this.reconnectAttempts - 1, 5));
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Scheduling Kafka reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
this.logger.info(
|
||||||
|
`Attempting to reconnect to Kafka (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||||
|
);
|
||||||
|
await this.connect();
|
||||||
|
this.logger.info('Successfully reconnected to Kafka');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({
|
||||||
|
local: 'KafkaController.scheduleReconnect',
|
||||||
|
message: `Reconnection attempt ${this.reconnectAttempts} failed`,
|
||||||
|
error: error.message || error,
|
||||||
|
});
|
||||||
|
this.isReconnecting = false;
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureConnection(): Promise<boolean> {
|
||||||
|
if (!this.producer) {
|
||||||
|
this.logger.warn('Kafka producer is not available, attempting to reconnect...');
|
||||||
|
if (!this.isReconnecting) {
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async emit({
|
||||||
|
instanceName,
|
||||||
|
origin,
|
||||||
|
event,
|
||||||
|
data,
|
||||||
|
serverUrl,
|
||||||
|
dateTime,
|
||||||
|
sender,
|
||||||
|
apiKey,
|
||||||
|
integration,
|
||||||
|
extra,
|
||||||
|
}: EmitData): Promise<void> {
|
||||||
|
if (integration && !integration.includes('kafka')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.ensureConnection())) {
|
||||||
|
this.logger.warn(`Failed to emit event ${event} for instance ${instanceName}: No Kafka connection`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceKafka = await this.get(instanceName);
|
||||||
|
const kafkaLocal = instanceKafka?.events;
|
||||||
|
const kafkaGlobal = configService.get<Kafka>('KAFKA').GLOBAL_ENABLED;
|
||||||
|
const kafkaEvents = configService.get<Kafka>('KAFKA').EVENTS;
|
||||||
|
const we = event.replace(/[.-]/gm, '_').toUpperCase();
|
||||||
|
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
...(extra ?? {}),
|
||||||
|
event,
|
||||||
|
instance: instanceName,
|
||||||
|
data,
|
||||||
|
server_url: serverUrl,
|
||||||
|
date_time: dateTime,
|
||||||
|
sender,
|
||||||
|
apikey: apiKey,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageValue = JSON.stringify(message);
|
||||||
|
|
||||||
|
// Instance-specific events
|
||||||
|
if (instanceKafka?.enabled && this.producer && Array.isArray(kafkaLocal) && kafkaLocal.includes(we)) {
|
||||||
|
const topicName = this.getTopicName(event, false, instanceName);
|
||||||
|
|
||||||
|
let retry = 0;
|
||||||
|
while (retry < 3) {
|
||||||
|
try {
|
||||||
|
await this.producer.send({
|
||||||
|
topic: topicName,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
key: instanceName,
|
||||||
|
value: messageValue,
|
||||||
|
headers: {
|
||||||
|
event,
|
||||||
|
instance: instanceName,
|
||||||
|
origin,
|
||||||
|
timestamp: dateTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logEnabled) {
|
||||||
|
const logData = {
|
||||||
|
local: `${origin}.sendData-Kafka`,
|
||||||
|
...message,
|
||||||
|
};
|
||||||
|
this.logger.log(logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({
|
||||||
|
local: 'KafkaController.emit',
|
||||||
|
message: `Error publishing local Kafka message (attempt ${retry + 1}/3)`,
|
||||||
|
error: error.message || error,
|
||||||
|
});
|
||||||
|
retry++;
|
||||||
|
if (retry >= 3) {
|
||||||
|
this.handleConnectionLoss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global events
|
||||||
|
if (kafkaGlobal && kafkaEvents[we] && this.producer) {
|
||||||
|
const topicName = this.getTopicName(event, true);
|
||||||
|
|
||||||
|
let retry = 0;
|
||||||
|
while (retry < 3) {
|
||||||
|
try {
|
||||||
|
await this.producer.send({
|
||||||
|
topic: topicName,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
key: `${instanceName}-${event}`,
|
||||||
|
value: messageValue,
|
||||||
|
headers: {
|
||||||
|
event,
|
||||||
|
instance: instanceName,
|
||||||
|
origin,
|
||||||
|
timestamp: dateTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logEnabled) {
|
||||||
|
const logData = {
|
||||||
|
local: `${origin}.sendData-Kafka-Global`,
|
||||||
|
...message,
|
||||||
|
};
|
||||||
|
this.logger.log(logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({
|
||||||
|
local: 'KafkaController.emit',
|
||||||
|
message: `Error publishing global Kafka message (attempt ${retry + 1}/3)`,
|
||||||
|
error: error.message || error,
|
||||||
|
});
|
||||||
|
retry++;
|
||||||
|
if (retry >= 3) {
|
||||||
|
this.handleConnectionLoss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cleanup(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.consumer) {
|
||||||
|
await this.consumer.disconnect();
|
||||||
|
this.consumer = null;
|
||||||
|
}
|
||||||
|
if (this.producer) {
|
||||||
|
await this.producer.disconnect();
|
||||||
|
this.producer = null;
|
||||||
|
}
|
||||||
|
this.kafkaClient = null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn({
|
||||||
|
local: 'KafkaController.cleanup',
|
||||||
|
message: 'Error during cleanup',
|
||||||
|
error: error.message || error,
|
||||||
|
});
|
||||||
|
this.producer = null;
|
||||||
|
this.consumer = null;
|
||||||
|
this.kafkaClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/api/integrations/event/kafka/kafka.router.ts
Normal file
36
src/api/integrations/event/kafka/kafka.router.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||||
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
|
import { EventDto } from '@api/integrations/event/event.dto';
|
||||||
|
import { HttpStatus } from '@api/routes/index.router';
|
||||||
|
import { eventManager } from '@api/server.module';
|
||||||
|
import { eventSchema, instanceSchema } from '@validate/validate.schema';
|
||||||
|
import { RequestHandler, Router } from 'express';
|
||||||
|
|
||||||
|
export class KafkaRouter extends RouterBroker {
|
||||||
|
constructor(...guards: RequestHandler[]) {
|
||||||
|
super();
|
||||||
|
this.router
|
||||||
|
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||||
|
const response = await this.dataValidate<EventDto>({
|
||||||
|
request: req,
|
||||||
|
schema: eventSchema,
|
||||||
|
ClassRef: EventDto,
|
||||||
|
execute: (instance, data) => eventManager.kafka.set(instance.instanceName, data),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(HttpStatus.CREATED).json(response);
|
||||||
|
})
|
||||||
|
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||||
|
const response = await this.dataValidate<InstanceDto>({
|
||||||
|
request: req,
|
||||||
|
schema: instanceSchema,
|
||||||
|
ClassRef: InstanceDto,
|
||||||
|
execute: (instance) => eventManager.kafka.get(instance.instanceName),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(HttpStatus.OK).json(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly router: Router = Router();
|
||||||
|
}
|
||||||
21
src/api/integrations/event/kafka/kafka.schema.ts
Normal file
21
src/api/integrations/event/kafka/kafka.schema.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { EventController } from '../event.controller';
|
||||||
|
|
||||||
|
export const kafkaSchema: JSONSchema7 = {
|
||||||
|
$id: v4(),
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
enabled: { type: 'boolean', enum: [true, false] },
|
||||||
|
events: {
|
||||||
|
type: 'array',
|
||||||
|
minItems: 0,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
enum: EventController.events,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['enabled'],
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const bucketExists = async () => {
|
|||||||
try {
|
try {
|
||||||
const list = await minioClient.listBuckets();
|
const list = await minioClient.listBuckets();
|
||||||
return list.find((bucket) => bucket.name === bucketName);
|
return list.find((bucket) => bucket.name === bucketName);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -826,7 +835,7 @@ export class ChannelStartupService {
|
|||||||
const msg = message.message;
|
const msg = message.message;
|
||||||
|
|
||||||
// Se só tem messageContextInfo, não é mídia válida
|
// Se só tem messageContextInfo, não é mídia válida
|
||||||
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
|
if (Object.keys(msg).length === 1 && Object.prototype.hasOwnProperty.call(msg, 'messageContextInfo')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export class ProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -153,6 +153,34 @@ export type Sqs = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Kafka = {
|
||||||
|
ENABLED: boolean;
|
||||||
|
CLIENT_ID: string;
|
||||||
|
BROKERS: string[];
|
||||||
|
CONNECTION_TIMEOUT: number;
|
||||||
|
REQUEST_TIMEOUT: number;
|
||||||
|
GLOBAL_ENABLED: boolean;
|
||||||
|
CONSUMER_GROUP_ID: string;
|
||||||
|
TOPIC_PREFIX: string;
|
||||||
|
NUM_PARTITIONS: number;
|
||||||
|
REPLICATION_FACTOR: number;
|
||||||
|
AUTO_CREATE_TOPICS: boolean;
|
||||||
|
EVENTS: EventsRabbitmq;
|
||||||
|
SASL?: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
MECHANISM: string;
|
||||||
|
USERNAME: string;
|
||||||
|
PASSWORD: string;
|
||||||
|
};
|
||||||
|
SSL?: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
REJECT_UNAUTHORIZED: boolean;
|
||||||
|
CA?: string;
|
||||||
|
KEY?: string;
|
||||||
|
CERT?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type Websocket = {
|
export type Websocket = {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
GLOBAL_EVENTS: boolean;
|
GLOBAL_EVENTS: boolean;
|
||||||
@ -372,6 +400,7 @@ export interface Env {
|
|||||||
RABBITMQ: Rabbitmq;
|
RABBITMQ: Rabbitmq;
|
||||||
NATS: Nats;
|
NATS: Nats;
|
||||||
SQS: Sqs;
|
SQS: Sqs;
|
||||||
|
KAFKA: Kafka;
|
||||||
WEBSOCKET: Websocket;
|
WEBSOCKET: Websocket;
|
||||||
WA_BUSINESS: WaBusiness;
|
WA_BUSINESS: WaBusiness;
|
||||||
LOG: Log;
|
LOG: Log;
|
||||||
@ -587,6 +616,68 @@ export class ConfigService {
|
|||||||
TYPEBOT_START: process.env?.SQS_GLOBAL_TYPEBOT_START === 'true',
|
TYPEBOT_START: process.env?.SQS_GLOBAL_TYPEBOT_START === 'true',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
KAFKA: {
|
||||||
|
ENABLED: process.env?.KAFKA_ENABLED === 'true',
|
||||||
|
CLIENT_ID: process.env?.KAFKA_CLIENT_ID || 'evolution-api',
|
||||||
|
BROKERS: process.env?.KAFKA_BROKERS?.split(',') || ['localhost:9092'],
|
||||||
|
CONNECTION_TIMEOUT: Number.parseInt(process.env?.KAFKA_CONNECTION_TIMEOUT || '3000'),
|
||||||
|
REQUEST_TIMEOUT: Number.parseInt(process.env?.KAFKA_REQUEST_TIMEOUT || '30000'),
|
||||||
|
GLOBAL_ENABLED: process.env?.KAFKA_GLOBAL_ENABLED === 'true',
|
||||||
|
CONSUMER_GROUP_ID: process.env?.KAFKA_CONSUMER_GROUP_ID || 'evolution-api-consumers',
|
||||||
|
TOPIC_PREFIX: process.env?.KAFKA_TOPIC_PREFIX || 'evolution',
|
||||||
|
NUM_PARTITIONS: Number.parseInt(process.env?.KAFKA_NUM_PARTITIONS || '1'),
|
||||||
|
REPLICATION_FACTOR: Number.parseInt(process.env?.KAFKA_REPLICATION_FACTOR || '1'),
|
||||||
|
AUTO_CREATE_TOPICS: process.env?.KAFKA_AUTO_CREATE_TOPICS === 'true',
|
||||||
|
EVENTS: {
|
||||||
|
APPLICATION_STARTUP: process.env?.KAFKA_EVENTS_APPLICATION_STARTUP === 'true',
|
||||||
|
INSTANCE_CREATE: process.env?.KAFKA_EVENTS_INSTANCE_CREATE === 'true',
|
||||||
|
INSTANCE_DELETE: process.env?.KAFKA_EVENTS_INSTANCE_DELETE === 'true',
|
||||||
|
QRCODE_UPDATED: process.env?.KAFKA_EVENTS_QRCODE_UPDATED === 'true',
|
||||||
|
MESSAGES_SET: process.env?.KAFKA_EVENTS_MESSAGES_SET === 'true',
|
||||||
|
MESSAGES_UPSERT: process.env?.KAFKA_EVENTS_MESSAGES_UPSERT === 'true',
|
||||||
|
MESSAGES_EDITED: process.env?.KAFKA_EVENTS_MESSAGES_EDITED === 'true',
|
||||||
|
MESSAGES_UPDATE: process.env?.KAFKA_EVENTS_MESSAGES_UPDATE === 'true',
|
||||||
|
MESSAGES_DELETE: process.env?.KAFKA_EVENTS_MESSAGES_DELETE === 'true',
|
||||||
|
SEND_MESSAGE: process.env?.KAFKA_EVENTS_SEND_MESSAGE === 'true',
|
||||||
|
SEND_MESSAGE_UPDATE: process.env?.KAFKA_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||||
|
CONTACTS_SET: process.env?.KAFKA_EVENTS_CONTACTS_SET === 'true',
|
||||||
|
CONTACTS_UPSERT: process.env?.KAFKA_EVENTS_CONTACTS_UPSERT === 'true',
|
||||||
|
CONTACTS_UPDATE: process.env?.KAFKA_EVENTS_CONTACTS_UPDATE === 'true',
|
||||||
|
PRESENCE_UPDATE: process.env?.KAFKA_EVENTS_PRESENCE_UPDATE === 'true',
|
||||||
|
CHATS_SET: process.env?.KAFKA_EVENTS_CHATS_SET === 'true',
|
||||||
|
CHATS_UPSERT: process.env?.KAFKA_EVENTS_CHATS_UPSERT === 'true',
|
||||||
|
CHATS_UPDATE: process.env?.KAFKA_EVENTS_CHATS_UPDATE === 'true',
|
||||||
|
CHATS_DELETE: process.env?.KAFKA_EVENTS_CHATS_DELETE === 'true',
|
||||||
|
CONNECTION_UPDATE: process.env?.KAFKA_EVENTS_CONNECTION_UPDATE === 'true',
|
||||||
|
LABELS_EDIT: process.env?.KAFKA_EVENTS_LABELS_EDIT === 'true',
|
||||||
|
LABELS_ASSOCIATION: process.env?.KAFKA_EVENTS_LABELS_ASSOCIATION === 'true',
|
||||||
|
GROUPS_UPSERT: process.env?.KAFKA_EVENTS_GROUPS_UPSERT === 'true',
|
||||||
|
GROUP_UPDATE: process.env?.KAFKA_EVENTS_GROUPS_UPDATE === 'true',
|
||||||
|
GROUP_PARTICIPANTS_UPDATE: process.env?.KAFKA_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
|
||||||
|
CALL: process.env?.KAFKA_EVENTS_CALL === 'true',
|
||||||
|
TYPEBOT_START: process.env?.KAFKA_EVENTS_TYPEBOT_START === 'true',
|
||||||
|
TYPEBOT_CHANGE_STATUS: process.env?.KAFKA_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
|
||||||
|
},
|
||||||
|
SASL:
|
||||||
|
process.env?.KAFKA_SASL_ENABLED === 'true'
|
||||||
|
? {
|
||||||
|
ENABLED: true,
|
||||||
|
MECHANISM: process.env?.KAFKA_SASL_MECHANISM || 'plain',
|
||||||
|
USERNAME: process.env?.KAFKA_SASL_USERNAME || '',
|
||||||
|
PASSWORD: process.env?.KAFKA_SASL_PASSWORD || '',
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
SSL:
|
||||||
|
process.env?.KAFKA_SSL_ENABLED === 'true'
|
||||||
|
? {
|
||||||
|
ENABLED: true,
|
||||||
|
REJECT_UNAUTHORIZED: process.env?.KAFKA_SSL_REJECT_UNAUTHORIZED !== 'false',
|
||||||
|
CA: process.env?.KAFKA_SSL_CA,
|
||||||
|
KEY: process.env?.KAFKA_SSL_KEY,
|
||||||
|
CERT: process.env?.KAFKA_SSL_CERT,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
WEBSOCKET: {
|
WEBSOCKET: {
|
||||||
ENABLED: process.env?.WEBSOCKET_ENABLED === 'true',
|
ENABLED: process.env?.WEBSOCKET_ENABLED === 'true',
|
||||||
GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true',
|
GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true',
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import fs from 'fs';
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const __dirname = path.resolve(process.cwd(), 'src', 'utils');
|
|
||||||
|
|
||||||
const languages = ['en', 'pt-BR', 'es'];
|
const languages = ['en', 'pt-BR', 'es'];
|
||||||
const translationsPath = path.join(__dirname, 'translations');
|
const translationsPath = path.join(__dirname, 'translations');
|
||||||
const configService: ConfigService = new ConfigService();
|
const configService: ConfigService = new ConfigService();
|
||||||
|
|||||||
@ -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';
|
||||||
@ -19,7 +20,7 @@ export async function keyExists(sessionId: string): Promise<any> {
|
|||||||
try {
|
try {
|
||||||
const key = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
|
const key = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
|
||||||
return !!key;
|
return !!key;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,7 +39,7 @@ export async function saveKey(sessionId: string, keyJson: any): Promise<any> {
|
|||||||
where: { sessionId: sessionId },
|
where: { sessionId: sessionId },
|
||||||
data: { creds: JSON.stringify(keyJson) },
|
data: { creds: JSON.stringify(keyJson) },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +50,7 @@ export async function getAuthKey(sessionId: string): Promise<any> {
|
|||||||
if (!register) return null;
|
if (!register) return null;
|
||||||
const auth = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
|
const auth = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
|
||||||
return JSON.parse(auth?.creds);
|
return JSON.parse(auth?.creds);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +60,7 @@ async function deleteAuthKey(sessionId: string): Promise<any> {
|
|||||||
const register = await keyExists(sessionId);
|
const register = await keyExists(sessionId);
|
||||||
if (!register) return;
|
if (!register) return;
|
||||||
await prismaRepository.session.delete({ where: { sessionId: sessionId } });
|
await prismaRepository.session.delete({ where: { sessionId: sessionId } });
|
||||||
} catch (error) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,17 +69,20 @@ async function fileExists(file: string): Promise<any> {
|
|||||||
try {
|
try {
|
||||||
const stat = await fs.stat(file);
|
const stat = await fs.stat(file);
|
||||||
if (stat.isFile()) return true;
|
if (stat.isFile()) return true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
@ -119,7 +123,7 @@ export default async function useMultiFileAuthStatePrisma(
|
|||||||
|
|
||||||
const parsedData = JSON.parse(rawData, BufferJSON.reviver);
|
const parsedData = JSON.parse(rawData, BufferJSON.reviver);
|
||||||
return parsedData;
|
return parsedData;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,11 +141,31 @@ export default async function useMultiFileAuthStatePrisma(
|
|||||||
} else {
|
} else {
|
||||||
await deleteAuthKey(sessionId);
|
await deleteAuthKey(sessionId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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