Compare commits

..

No commits in common. "main" and "2.3.3" have entirely different histories.
main ... 2.3.3

91 changed files with 3560 additions and 7086 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -190,60 +190,6 @@ PUSHER_EVENTS_CALL=true
PUSHER_EVENTS_TYPEBOT_START=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
# Token used to validate the webhook on the Facebook APP
WA_BUSINESS_TOKEN_WEBHOOK=evolution

View File

@ -26,7 +26,7 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'import/first': 'error',
'import/no-duplicates': 'error',
'simple-import-sort/imports': 'error',

View File

@ -59,7 +59,7 @@ body:
value: |
- OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0]
- Node.js version: [e.g. 18.17.0]
- Evolution API version: [e.g. 2.3.7]
- Evolution API version: [e.g. 2.3.3]
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
- Connection type: [e.g. Baileys, WhatsApp Business API]
validations:

View File

@ -13,8 +13,6 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install Node
uses: actions/setup-node@v5

View File

@ -15,8 +15,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
submodules: recursive
- name: Docker meta
id: meta

View File

@ -15,8 +15,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
submodules: recursive
- name: Docker meta
id: meta

View File

@ -15,8 +15,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
submodules: recursive
- name: Docker meta
id: meta

View File

@ -26,8 +26,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
submodules: recursive
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@ -49,7 +47,5 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
submodules: recursive
- name: Dependency Review
uses: actions/dependency-review-action@v4

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "evolution-manager-v2"]
path = evolution-manager-v2
url = https://github.com/EvolutionAPI/evolution-manager-v2.git

View File

@ -1,296 +1,3 @@
# 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)
### Features

View File

@ -1,51 +0,0 @@
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

View File

@ -2,7 +2,7 @@ version: "3.7"
services:
evolution_v2:
image: evoapicloud/evolution-api:v2.3.7
image: evoapicloud/evolution-api:v2.3.1
volumes:
- evolution_instances:/evolution/instances
networks:

View File

@ -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.
© 2025 Evolution API
© 2024 Evolution API

View File

@ -55,9 +55,6 @@ Evolution API supports various integrations to enhance its functionality. Below
- [RabbitMQ](https://www.rabbitmq.com/):
- 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/):
- Receive events from the Evolution API via Amazon SQS.

View File

@ -15,16 +15,6 @@ services:
expose:
- 8080
frontend:
container_name: evolution_frontend
image: evolution/manager:local
build: ./evolution-manager-v2
restart: always
ports:
- "3000:80"
networks:
- evolution-net
volumes:
evolution_instances:

View File

@ -3,7 +3,7 @@ version: "3.8"
services:
api:
container_name: evolution_api
image: evoapicloud/evolution-api:latest
image: evolution/api:metrics
restart: always
depends_on:
- redis
@ -20,15 +20,6 @@ services:
expose:
- "8080"
frontend:
container_name: evolution_frontend
image: evoapicloud/evolution-manager:latest
restart: always
ports:
- "3000:80"
networks:
- evolution-net
redis:
container_name: evolution_redis
image: redis:latest

View File

@ -1,302 +0,0 @@
# ===========================================
# 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 +0,0 @@
Subproject commit f054b9bc28083152d4948f835e3346fd0add39db

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

381
manager/dist/assets/index-D-oOjDYe.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<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" />
<title>Evolution Manager</title>
<script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
</head>
<body>
<div id="root"></div>

View File

@ -1,8 +0,0 @@
#! /bin/bash
cd evolution-manager-v2
npm install
npm run build
cd ..
rm -rf manager/dist
cp -r evolution-manager-v2/dist manager/dist

5343
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "2.3.7",
"version": "2.3.3",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js",
"type": "commonjs",
@ -56,7 +56,7 @@
"eslint --fix"
],
"src/**/*.ts": [
"sh -c 'tsc --noEmit'"
"sh -c 'npm run build'"
]
},
"config": {
@ -77,7 +77,7 @@
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "7.0.0-rc.9",
"baileys": "^7.0.0-rc.3",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
@ -90,14 +90,11 @@
"fluent-ffmpeg": "^2.1.3",
"form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6",
"fetch-socks": "^1.3.2",
"i18next": "^23.7.19",
"jimp": "^1.6.0",
"json-schema": "^0.4.0",
"jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.2.4",
"libphonenumber-js": "^1.12.25",
"link-preview-js": "^3.0.13",
"long": "^5.2.3",
"mediainfo.js": "^0.3.4",
@ -123,7 +120,6 @@
"socks-proxy-agent": "^8.0.5",
"swagger-ui-express": "^5.0.1",
"tsup": "^8.3.5",
"undici": "^7.16.0",
"uuid": "^13.0.0"
},
"devDependencies": {

View File

@ -1,231 +0,0 @@
/*
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;

View File

@ -88,7 +88,6 @@ model Instance {
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Kafka Kafka?
Websocket Websocket?
Typebot Typebot[]
Session Session?
@ -106,11 +105,8 @@ model Instance {
EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
N8n N8n[]
N8nSetting N8nSetting?
Evoai Evoai[]
EvoaiSetting EvoaiSetting?
Pusher Pusher?
N8n N8n[]
}
model Session {
@ -313,16 +309,6 @@ model Sqs {
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 {
id String @id @default(cuid())
enabled Boolean @default(false)
@ -661,7 +647,7 @@ model IsOnWhatsapp {
model N8n {
id String @id @default(cuid())
enabled Boolean @default(true) @db.TinyInt()
enabled Boolean @default(true) @db.TinyInt(1)
description String? @db.VarChar(255)
webhookUrl String? @db.VarChar(255)
basicAuthUser String? @db.VarChar(255)
@ -680,7 +666,7 @@ model N8n {
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@ -700,7 +686,7 @@ model N8nSetting {
ignoreJids Json?
splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Int
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
n8nIdFallback String? @db.VarChar(100)
@ -710,7 +696,7 @@ model N8nSetting {
model Evoai {
id String @id @default(cuid())
enabled Boolean @default(true) @db.TinyInt()
enabled Boolean @default(true) @db.TinyInt(1)
description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
@ -728,7 +714,7 @@ model Evoai {
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@ -748,7 +734,7 @@ model EvoaiSetting {
ignoreJids Json?
splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Int
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
evoaiIdFallback String? @db.VarChar(100)

View File

@ -1,17 +0,0 @@
-- 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;

View File

@ -1,16 +0,0 @@
-- 1. Cleanup: Remove duplicate chats, keeping the most recently updated one
DELETE FROM "Chat"
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY "instanceId", "remoteJid"
ORDER BY "updatedAt" DESC
) as row_num
FROM "Chat"
) t
WHERE t.row_num > 1
);
-- 2. Create the unique index (Constraint)
CREATE UNIQUE INDEX "Chat_instanceId_remoteJid_key" ON "Chat"("instanceId", "remoteJid");

View File

@ -88,7 +88,6 @@ model Instance {
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Kafka Kafka?
Websocket Websocket?
Typebot Typebot[]
Session Session?
@ -132,7 +131,6 @@ model Chat {
instanceId String
unreadMessages Int @default(0)
@@unique([instanceId, remoteJid])
@@index([instanceId])
@@index([remoteJid])
}
@ -314,16 +312,6 @@ model Sqs {
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 {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean

View File

@ -89,7 +89,6 @@ model Instance {
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Kafka Kafka?
Websocket Websocket?
Typebot Typebot[]
Session Session?
@ -314,16 +313,6 @@ model Sqs {
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 {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean

View File

@ -92,15 +92,6 @@ export class InstanceController {
instanceId: instanceId,
});
const instanceDto: InstanceDto = {
instanceName: instance.instanceName,
instanceId: instance.instanceId,
connectionStatus:
typeof instance.connectionStatus === 'string'
? instance.connectionStatus
: instance.connectionStatus?.state || 'unknown',
};
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
const testProxy = await this.proxyService.testProxy({
host: instanceData.proxyHost,
@ -112,7 +103,8 @@ export class InstanceController {
if (!testProxy) {
throw new BadRequestException('Invalid proxy');
}
await this.proxyService.createProxy(instanceDto, {
await this.proxyService.createProxy(instance, {
enabled: true,
host: instanceData.proxyHost,
port: instanceData.proxyPort,
@ -133,7 +125,7 @@ export class InstanceController {
wavoipToken: instanceData.wavoipToken || '',
};
await this.settingsService.create(instanceDto, settings);
await this.settingsService.create(instance, settings);
let webhookWaBusiness = null,
accessTokenWaBusiness = '';
@ -163,10 +155,7 @@ export class InstanceController {
integration: instanceData.integration,
webhookWaBusiness,
accessTokenWaBusiness,
status:
typeof instance.connectionStatus === 'string'
? instance.connectionStatus
: instance.connectionStatus?.state || 'unknown',
status: instance.connectionStatus.state,
},
hash,
webhook: {
@ -228,7 +217,7 @@ export class InstanceController {
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
try {
this.chatwootService.create(instanceDto, {
this.chatwootService.create(instance, {
enabled: true,
accountId: instanceData.chatwootAccountId,
token: instanceData.chatwootToken,
@ -257,10 +246,7 @@ export class InstanceController {
integration: instanceData.integration,
webhookWaBusiness,
accessTokenWaBusiness,
status:
typeof instance.connectionStatus === 'string'
? instance.connectionStatus
: instance.connectionStatus?.state || 'unknown',
status: instance.connectionStatus.state,
},
hash,
webhook: {
@ -352,38 +338,20 @@ export class InstanceController {
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
}
if (state === 'close') {
if (state == 'close') {
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
}
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') {
} else if (state == 'open') {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
this.logger.info('restarting instance' + instanceName);
instance.client?.ws?.close();
instance.client?.end(new Error('restart'));
return await this.connectToWhatsapp({ instanceName });
} else if (state == 'connecting') {
instance.client?.ws?.close();
instance.client?.end(new Error('restart'));
return await this.connectToWhatsapp({ instanceName });
}
return {
instance: {
instanceName: instanceName,
status: state,
},
};
} catch (error) {
this.logger.error(error);
return { error: true, message: error.toString() };
@ -441,7 +409,7 @@ export class InstanceController {
}
try {
await this.waMonitor.waInstances[instanceName]?.logoutInstance();
this.waMonitor.waInstances[instanceName]?.logoutInstance();
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
} catch (error) {

View File

@ -12,15 +12,4 @@ export class TemplateController {
public async findTemplate(instance: InstanceDto) {
return this.templateService.find(instance);
}
public async editTemplate(
instance: InstanceDto,
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
) {
return this.templateService.edit(instance, data);
}
public async deleteTemplate(instance: InstanceDto, data: { name: string; hsmId?: string }) {
return this.templateService.delete(instance, data);
}
}

View File

@ -12,7 +12,6 @@ export class InstanceDto extends IntegrationDto {
token?: string;
status?: string;
ownerJid?: string;
connectionStatus?: string;
profileName?: string;
profilePicUrl?: string;
// settings

View File

@ -6,16 +6,3 @@ export class TemplateDto {
components: any;
webhookUrl?: string;
}
export class TemplateEditDto {
templateId: string;
category?: 'AUTHENTICATION' | 'MARKETING' | 'UTILITY';
allowCategoryChange?: boolean;
ttl?: number;
components?: any;
}
export class TemplateDeleteDto {
name: string;
hsmId?: string;
}

View File

@ -16,7 +16,6 @@ import { Events, wa } from '@api/types/wa.types';
import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
@ -172,8 +171,6 @@ export class EvolutionStartupService extends ChannelStartupService {
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({
@ -326,8 +323,8 @@ export class EvolutionStartupService extends ChannelStartupService {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
base64: isBase64(message.media) ? message.media : null,
mediaUrl: isURL(message.media) ? message.media : null,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'imageMessage',
@ -340,8 +337,8 @@ export class EvolutionStartupService extends ChannelStartupService {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
base64: isBase64(message.media) ? message.media : null,
mediaUrl: isURL(message.media) ? message.media : null,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'videoMessage',
@ -354,8 +351,8 @@ export class EvolutionStartupService extends ChannelStartupService {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
base64: isBase64(message.media) ? message.media : null,
mediaUrl: isURL(message.media) ? message.media : null,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'audioMessage',
@ -375,8 +372,8 @@ export class EvolutionStartupService extends ChannelStartupService {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
base64: isBase64(message.media) ? message.media : null,
mediaUrl: isURL(message.media) ? message.media : null,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'documentMessage',
@ -452,7 +449,7 @@ export class EvolutionStartupService extends ChannelStartupService {
}
}
const { base64 } = messageRaw.message;
const base64 = messageRaw.message.base64;
delete messageRaw.message.base64;
if (base64 || file || audioFile) {

View File

@ -24,7 +24,6 @@ import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusine
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid';
import { status } from '@utils/renderStatus';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
@ -516,9 +515,7 @@ export class BusinessStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName);
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
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
@ -556,19 +553,11 @@ export class BusinessStartupService extends ChannelStartupService {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
} else {
if (this.localWebhook.enabled && this.localWebhook.webhookBase64) {
const buffer = await this.downloadMediaMessage(received?.messages[0]);
messageRaw.message.base64 = buffer.toString('base64');
}
const buffer = await this.downloadMediaMessage(received?.messages[0]);
messageRaw.message.base64 = buffer.toString('base64');
// Processar OpenAI speech-to-text para áudio mesmo sem S3
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
let openAiBase64 = messageRaw.message.base64;
if (!openAiBase64) {
const buffer = await this.downloadMediaMessage(received?.messages[0]);
openAiBase64 = buffer.toString('base64');
}
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
@ -584,7 +573,7 @@ export class BusinessStartupService extends ChannelStartupService {
openAiDefaultSettings.OpenaiCreds,
{
message: {
base64: openAiBase64,
base64: messageRaw.message.base64,
...messageRaw,
},
},
@ -666,8 +655,6 @@ export class BusinessStartupService extends ChannelStartupService {
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({
@ -1026,7 +1013,6 @@ export class BusinessStartupService extends ChannelStartupService {
[message['mediaType']]: {
[message['type']]: message['id'],
...(message['mediaType'] !== 'audio' &&
message['mediaType'] !== 'video' &&
message['fileName'] &&
!isImage && { filename: message['fileName'] }),
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
@ -1617,14 +1603,9 @@ export class BusinessStartupService extends ChannelStartupService {
const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
const mediaMessage = msg.message[messageType];
if (!msg.message?.base64) {
const buffer = await this.downloadMediaMessage({ type: messageType, ...msg.message });
msg.message.base64 = buffer.toString('base64');
}
return {
mediaType: msg.messageType,
fileName: mediaMessage?.fileName || mediaMessage?.filename,
fileName: mediaMessage?.fileName,
caption: mediaMessage?.caption,
size: {
fileLength: mediaMessage?.fileLength,

View File

@ -1,5 +1,5 @@
import { Logger } from '@config/logger.config';
import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys';
import { BaileysEventMap, MessageUpsertType, proto } from 'baileys';
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
@ -12,29 +12,13 @@ export class BaileysMessageProcessor {
private subscription?: Subscription;
protected messageSubject = new Subject<{
messages: WAMessage[];
messages: proto.IWebMessageInfo[];
type: MessageUpsertType;
requestId?: string;
settings: any;
}>();
mount({ onMessageReceive }: MountProps) {
// Se já existe subscription, fazer cleanup primeiro
if (this.subscription && !this.subscription.closed) {
this.subscription.unsubscribe();
}
// Se o Subject foi completado, recriar
if (this.messageSubject.closed) {
this.processorLogs.warn('MessageSubject was closed, recreating...');
this.messageSubject = new Subject<{
messages: WAMessage[];
type: MessageUpsertType;
requestId?: string;
settings: any;
}>();
}
this.subscription = this.messageSubject
.pipe(
tap(({ messages }) => {

View File

@ -71,7 +71,7 @@ export const useVoiceCallsBaileys = async (
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids);
const response = await baileys_sock.assertSessions(jids, force);
callback(response);

View File

@ -49,7 +49,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
try {
JSON.parse(str);
return true;
} catch {
} catch (e) {
return false;
}
}
@ -180,7 +180,6 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
remoteJid: string,
message: string,
settings: SettingsType,
linkPreview: boolean = true,
): Promise<void> {
if (!message) return;
@ -203,7 +202,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
if (mediaType) {
// Send accumulated text before sending media
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages, linkPreview);
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
textBuffer = '';
}
@ -211,7 +210,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
try {
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
audio: url,
caption: altText,
@ -219,7 +218,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
} else {
await instance.mediaMessage(
{
number: remoteJid.includes('@lid') ? remoteJid : remoteJid.split('@')[0],
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
mediatype: mediaType,
media: url,
@ -253,56 +252,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
// Send any remaining text
if (textBuffer.trim()) {
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);
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
}
}
@ -315,24 +265,67 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
text: string,
settings: any,
splitMessages: boolean,
linkPreview: boolean = true,
): Promise<void> {
const timePerChar = settings?.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const messageParts = this.splitMessageByDoubleLineBreaks(text);
const multipleMessages = text.split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
if (!message.trim()) continue;
this.logger.debug(`[BaseChatbot] Splitting message into ${messageParts.length} parts`);
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
for (let index = 0; index < messageParts.length; index++) {
const message = messageParts[index];
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
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) => {
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 {
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
this.logger.debug(`[BaseChatbot] All message parts sent successfully`);
} else {
this.logger.debug(`[BaseChatbot] Sending single message`);
await this.sendSingleMessage(instance, remoteJid, text, settings, linkPreview);
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);
}
}
}

View File

@ -91,19 +91,19 @@ export class ChatbotController {
pushName,
isIntegration,
};
evolutionBotController.emit(emitData);
await evolutionBotController.emit(emitData);
typebotController.emit(emitData);
await typebotController.emit(emitData);
openaiController.emit(emitData);
await openaiController.emit(emitData);
difyController.emit(emitData);
await difyController.emit(emitData);
n8nController.emit(emitData);
await n8nController.emit(emitData);
evoaiController.emit(emitData);
await evoaiController.emit(emitData);
flowiseController.emit(emitData);
await flowiseController.emit(emitData);
}
public processDebounce(

View File

@ -1,6 +1,10 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
import { PrismaRepository } from '@api/repository/repository.service';
import { waMonitor } from '@api/server.module';
import { CacheService } from '@api/services/cache.service';
import { CacheEngine } from '@cache/cacheengine';
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
import { BadRequestException } from '@exceptions';
import { isURL } from 'class-validator';
@ -9,6 +13,7 @@ export class ChatwootController {
constructor(
private readonly chatwootService: ChatwootService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
@ -79,6 +84,9 @@ export class ChatwootController {
public async receiveWebhook(instance: InstanceDto, data: any) {
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
return this.chatwootService.receiveWebhook(instance, data);
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache);
return chatwootService.receiveWebhook(instance, data);
}
}

View File

@ -1,5 +1,6 @@
import { InstanceDto } from '@api/dto/instance.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 { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
@ -23,11 +24,10 @@ import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageM
import i18next from '@utils/i18n';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { WAMessageContent, WAMessageKey } from 'baileys';
import { proto } from 'baileys';
import dayjs from 'dayjs';
import FormData from 'form-data';
import { Jimp, JimpMime } from 'jimp';
import { parsePhoneNumberFromString } from 'libphonenumber-js';
import Long from 'long';
import mimeTypes from 'mime-types';
import path from 'path';
@ -44,9 +44,6 @@ interface ChatwootMessage {
export class 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;
constructor(
@ -133,7 +130,7 @@ export class ChatwootService {
public async find(instance: InstanceDto): Promise<ChatwootDto> {
try {
return await this.waMonitor.waInstances[instance.instanceName].findChatwoot();
} catch {
} catch (error) {
this.logger.error('chatwoot not found');
return { enabled: null, url: '' };
}
@ -346,16 +343,6 @@ export class ChatwootService {
return contact;
} catch (error) {
if ((error.status === 422 || error.response?.status === 422) && jid) {
this.logger.warn(`Contact with identifier ${jid} creation failed (422). Checking if it already exists...`);
const existingContact = await this.findContactByIdentifier(instance, jid);
if (existingContact) {
const contactId = existingContact.id;
await this.addLabelToContact(this.provider.nameInbox, contactId);
return existingContact;
}
}
this.logger.error('Error creating contact');
console.log(error);
return null;
@ -383,7 +370,7 @@ export class ChatwootService {
});
return contact;
} catch {
} catch (error) {
return null;
}
}
@ -420,60 +407,11 @@ export class ChatwootService {
}
return true;
} catch {
} catch (error) {
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) {
const client = await this.clientCw(instance);
@ -630,31 +568,27 @@ export class ChatwootService {
}
public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.addressingMode === 'lid';
const isGroup = body.key.remoteJid.endsWith('@g.us');
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
const { remoteJid } = body.key;
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 seconds
const client = await this.clientCw(instance);
if (!client) return null;
const maxWaitTime = 5000; // 5 secounds
try {
// Processa atualização de contatos já criados @lid
if (phoneNumber && remoteJid && !isGroup) {
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
if (contact && contact.identifier !== remoteJid) {
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
);
const updateContact = await this.updateContact(instance, contact.id, {
identifier: phoneNumber,
phone_number: `+${phoneNumber.split('@')[0]}`,
identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`,
});
if (updateContact === null) {
const baseContact = await this.findContact(instance, phoneNumber.split('@')[0]);
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
if (baseContact) {
await this.mergeContacts(baseContact.id, contact.id);
this.logger.verbose(
@ -670,25 +604,7 @@ export class ChatwootService {
// If it already exists in the cache, return conversationId
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
let conversationExists: 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);
}
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
return conversationId;
}
@ -701,7 +617,7 @@ export class ChatwootService {
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
break;
}
await new Promise((res) => setTimeout(res, this.LOCK_POLLING_DELAY_MS));
await new Promise((res) => setTimeout(res, 300));
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
@ -723,7 +639,11 @@ export class ChatwootService {
return (await this.cache.get(cacheKey)) as number;
}
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].split(':')[0];
const client = await this.clientCw(instance);
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;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
@ -731,22 +651,19 @@ export class ChatwootService {
if (isGroup) {
this.logger.verbose(`Processing group conversation`);
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
nameContact = `${group.subject} (GROUP)`;
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
participantJid.split('@')[0],
body.key.participant.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
if (findParticipant) {
this.logger.verbose(
`Found participant: ID:${findParticipant.id} - Name: ${findParticipant.name} - identifier: ${findParticipant.identifier}`,
);
if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
name: body.pushName,
@ -756,12 +673,12 @@ export class ChatwootService {
} else {
await this.createContact(
instance,
participantJid.split('@')[0].split(':')[0],
body.key.participant.split('@')[0],
filterInbox.id,
false,
body.pushName,
picture_url.profilePictureUrl || null,
participantJid,
body.key.participant,
);
}
}
@ -769,17 +686,23 @@ export class ChatwootService {
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(`Searching contact for: ${chatId}`);
let contact = await this.findContact(instance, chatId);
if (contact) {
this.logger.verbose(`Found contact: ID:${contact.id} - Name:${contact.name}`);
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
if (!body.key.fromMe) {
const waProfilePictureFile =
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate = !contact.name || contact.name === chatId;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
if (pictureNeedsUpdate || nameNeedsUpdate) {
@ -798,7 +721,7 @@ export class ChatwootService {
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
phoneNumber,
remoteJid,
);
}
@ -814,6 +737,7 @@ export class ChatwootService {
accountId: this.provider.accountId,
id: contactId,
})) as any;
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
if (!contactConversations || !contactConversations.payload) {
this.logger.error(`No conversations found or payload is undefined`);
@ -825,9 +749,7 @@ export class ChatwootService {
);
if (inboxConversation) {
if (this.provider.reopenConversation) {
this.logger.verbose(
`Found conversation in reopenConversation mode: ID: ${inboxConversation.id} - Name: ${inboxConversation.meta.sender.name} - Identifier: ${inboxConversation.meta.sender.identifier}`,
);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
@ -847,7 +769,7 @@ export class ChatwootService {
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id, 1800);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
}
@ -861,6 +783,14 @@ export class ChatwootService {
data['status'] = 'pending';
}
/*
Triple check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
@ -872,7 +802,7 @@ export class ChatwootService {
}
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id, 1800);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} finally {
await this.cache.delete(lockKey);
@ -1228,7 +1158,7 @@ export class ChatwootService {
const data: SendAudioDto = {
number: number,
audio: media,
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
delay: 1200,
quoted: options?.quoted,
};
@ -1239,7 +1169,7 @@ export class ChatwootService {
return messageSent;
}
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
type = 'document';
}
@ -1264,7 +1194,6 @@ export class ChatwootService {
return messageSent;
} catch (error) {
this.logger.error(error);
throw error; // Re-throw para que o erro seja tratado pelo caller
}
}
@ -1346,7 +1275,6 @@ export class ChatwootService {
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
const waInstance = this.waMonitor.waInstances[instance.instanceName];
instance.instanceId = waInstance.instanceId;
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
const message = await this.prismaRepository.message.findFirst({
@ -1357,7 +1285,7 @@ export class ChatwootService {
});
if (message) {
const key = message.key as WAMessageKey;
const key = message.key as ExtendedMessageKey;
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
@ -1489,6 +1417,7 @@ export class ChatwootService {
await this.updateChatwootMessageId(
{
...messageSent,
owner: instance.instanceName,
},
{
messageId: body.id,
@ -1503,7 +1432,7 @@ export class ChatwootService {
const data: SendTextDto = {
number: chatId,
text: formatText,
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
delay: 1200,
quoted: await this.getQuotedMessage(body, instance),
};
@ -1523,6 +1452,7 @@ export class ChatwootService {
await this.updateChatwootMessageId(
{
...messageSent,
instanceId: instance.instanceId,
},
{
messageId: body.id,
@ -1553,7 +1483,7 @@ export class ChatwootService {
},
});
if (lastMessage && !lastMessage.chatwootIsRead) {
const key = lastMessage.key as WAMessageKey;
const key = lastMessage.key as ExtendedMessageKey;
waInstance?.markMessageAsRead({
readMessages: [
@ -1590,7 +1520,7 @@ export class ChatwootService {
const data: SendTextDto = {
number: chatId,
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
delay: 1200,
};
sendTelemetry('/message/sendText');
@ -1611,14 +1541,14 @@ export class ChatwootService {
chatwootMessageIds: ChatwootMessage,
instance: InstanceDto,
) {
const key = message.key as WAMessageKey;
const key = message.key as ExtendedMessageKey;
if (!chatwootMessageIds.messageId || !key?.id) {
return;
}
// Use raw SQL to avoid JSON path issues
const result = await this.prismaRepository.$executeRaw`
await this.prismaRepository.$executeRaw`
UPDATE "Message"
SET
"chatwootMessageId" = ${chatwootMessageIds.messageId},
@ -1630,14 +1560,8 @@ export class ChatwootService {
AND "key"->>'id' = ${key.id}
`;
this.logger.verbose(`Update result: ${result} rows affected`);
if (this.isImportHistoryAvailable()) {
try {
await chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
} catch (error) {
this.logger.error(`Error updating Chatwoot message source ID: ${error}`);
}
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
}
}
@ -1685,13 +1609,12 @@ export class ChatwootService {
},
});
const key = message?.key as WAMessageKey;
const messageContent = message?.message as WAMessageContent;
const key = message?.key as ExtendedMessageKey;
if (messageContent && key?.id) {
if (message && key?.id) {
return {
key: key,
message: messageContent,
key: message.key as proto.IMessageKey,
message: message.message as proto.IMessage,
};
}
}
@ -1717,10 +1640,6 @@ export class ChatwootService {
return result;
}
private isInteractiveButtonMessage(messageType: string, message: any) {
return messageType === 'interactiveMessage' && message.interactiveMessage?.nativeFlowMessage?.buttons?.length > 0;
}
private getAdsMessage(msg: any) {
interface AdsMessage {
title: string;
@ -1994,7 +1913,6 @@ export class ChatwootService {
}
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') {
return;
}
@ -2039,9 +1957,8 @@ export class ChatwootService {
const adsMessage = this.getAdsMessage(body);
const reactionMessage = this.getReactionMessage(body.message);
const isInteractiveButtonMessage = this.isInteractiveButtonMessage(body.messageType, body.message);
if (!bodyMessage && !isMedia && !reactionMessage && !isInteractiveButtonMessage) {
if (!bodyMessage && !isMedia && !reactionMessage) {
this.logger.warn('no body message found');
return;
}
@ -2086,20 +2003,23 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber =
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
? body.key.participantAlt.split('@')[0].split(':')[0]
: body.key.participant.split('@')[0].split(':')[0];
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
if (!body.key.fromMe) {
content = bodyMessage
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
: `**${formattedPhoneNumber} - ${participantName}:**`;
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
} else {
content = bodyMessage || '';
content = `${bodyMessage}`;
}
const send = await this.sendData(
@ -2166,50 +2086,6 @@ export class ChatwootService {
return;
}
if (isInteractiveButtonMessage) {
const buttons = body.message.interactiveMessage.nativeFlowMessage.buttons;
this.logger.info('is Interactive Button Message: ' + JSON.stringify(buttons));
for (const button of buttons) {
const buttonParams = JSON.parse(button.buttonParamsJson);
const paymentSettings = buttonParams.payment_settings;
if (button.name === 'payment_info' && paymentSettings[0].type === 'pix_static_code') {
const pixSettings = paymentSettings[0].pix_static_code;
const pixKeyType = (() => {
switch (pixSettings.key_type) {
case 'EVP':
return 'Chave Aleatória';
case 'EMAIL':
return 'E-mail';
case 'PHONE':
return 'Telefone';
default:
return pixSettings.key_type;
}
})();
const pixKey = pixSettings.key_type === 'PHONE' ? pixSettings.key.replace('+55', '') : pixSettings.key;
const content = `*${pixSettings.merchant_name}*\nChave PIX: ${pixKey} (${pixKeyType})`;
const send = await this.createMessage(
instance,
getConversation,
content,
messageType,
false,
[],
body,
'WAID:' + body.key.id,
quotedMsg,
);
if (!send) this.logger.warn('message not sent');
} else {
this.logger.warn('Interactive Button Message not mapped');
}
}
return;
}
const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl;
if (isAdsMessage) {
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
@ -2268,11 +2144,16 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber =
body.key.addressingMode === 'lid' && !body.key.fromMe && body.key.participantAlt
? body.key.participantAlt.split('@')[0].split(':')[0]
: body.key.participant.split('@')[0].split(':')[0];
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
@ -2354,21 +2235,9 @@ export class ChatwootService {
}
if (event === 'messages.edit' || event === 'send.message.update') {
const editedMessageContentRaw =
body?.editedMessage?.conversation ??
body?.editedMessage?.extendedTextMessage?.text ??
body?.editedMessage?.imageMessage?.caption ??
body?.editedMessage?.videoMessage?.caption ??
body?.editedMessage?.documentMessage?.caption ??
(typeof body?.text === 'string' ? body.text : undefined);
const editedMessageContent = (editedMessageContentRaw ?? '').trim();
if (!editedMessageContent) {
this.logger.info('[CW.EDIT] Conteúdo vazio — ignorando (DELETE tratará se for revoke).');
return;
}
const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
const message = await this.getMessageByKeyId(instance, body?.key?.id);
if (!message) {
@ -2376,14 +2245,11 @@ export class ChatwootService {
return;
}
const key = message.key as WAMessageKey;
const key = message.key as ExtendedMessageKey;
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
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}`;
if (message && message.chatwootConversationId) {
const send = await this.createMessage(
instance,
message.chatwootConversationId,
@ -2435,7 +2301,7 @@ export class ChatwootService {
const url =
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
`/conversations/${conversationId}/update_last_seen`;
await chatwootRequest(this.getClientCwConfig(), {
chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: url,
});
@ -2461,30 +2327,15 @@ export class ChatwootService {
await this.createBotMessage(instance, msgStatus, 'incoming');
}
if (event === 'connection.update' && body.status === 'open') {
const waInstance = this.waMonitor.waInstances[instance.instanceName];
if (!waInstance) return;
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');
await this.createBotMessage(instance, msgConnection, 'incoming');
waInstance.qrCode.count = 0;
waInstance.lastConnectionNotification = now;
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)`,
);
if (event === 'connection.update') {
if (body.status === 'open') {
// if we have qrcode count then we understand that a new connection was established
if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) {
const msgConnection = i18next.t('cw.inbox.connected');
await this.createBotMessage(instance, msgConnection, 'incoming');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
chatwootImport.clearAll(instance);
}
}
}
@ -2527,13 +2378,7 @@ export class ChatwootService {
}
}
public normalizeJidIdentifier(remoteJid: string) {
if (!remoteJid) {
return '';
}
if (remoteJid.includes('@lid')) {
return remoteJid;
}
public getNumberFromRemoteJid(remoteJid: string) {
return remoteJid.replace(/:\d+/, '').split('@')[0];
}
@ -2707,7 +2552,7 @@ export class ChatwootService {
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
} catch {
} catch (error) {
return;
}
}

View File

@ -112,19 +112,12 @@ class ChatwootImport {
const bindInsert = [provider.accountId];
for (const contact of contactsChunk) {
const isGroup = this.isIgnorePhoneNumber(contact.remoteJid);
const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName;
bindInsert.push(contactName);
bindInsert.push(contact.pushName);
const bindName = `$${bindInsert.length}`;
let bindPhoneNumber: string;
if (!isGroup) {
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
bindPhoneNumber = `$${bindInsert.length}`;
} else {
bindPhoneNumber = 'NULL';
}
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
const bindPhoneNumber = `$${bindInsert.length}`;
bindInsert.push(contact.remoteJid);
const bindIdentifier = `$${bindInsert.length}`;
@ -137,7 +130,7 @@ class ChatwootImport {
DO UPDATE SET
name = EXCLUDED.name,
phone_number = EXCLUDED.phone_number,
updated_at = NOW()`;
identifier = EXCLUDED.identifier`;
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;

View File

@ -4,7 +4,6 @@ import { Integration } from '@api/types/wa.types';
import { ConfigService, HttpServer } from '@config/env.config';
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { isURL } from 'class-validator';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
@ -79,35 +78,15 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
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 = [
{
type: 'image',
transfer_method: 'remote_url',
url: mediaBase64,
},
];
}
} else {
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: media[1].split('?')[0],
},
];
}
payload.query = media[2] || content;
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: contentSplit[1].split('?')[0],
},
];
payload.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
@ -128,7 +107,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
const conversationId = response?.data?.conversation_id;
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
@ -161,35 +140,15 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
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 = [
{
type: 'image',
transfer_method: 'remote_url',
url: mediaBase64,
},
];
}
} else {
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: media[1].split('?')[0],
},
];
payload.inputs.query = media[2] || content;
}
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: contentSplit[1].split('?')[0],
},
];
payload.inputs.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
@ -210,7 +169,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
const conversationId = response?.data?.conversation_id;
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
@ -243,26 +202,15 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
// Handle image messages
if (this.isImageMessage(content)) {
const media = content.split('|');
if (msg.message.mediaUrl || msg.message.base64) {
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: msg.message.mediaUrl || msg.message.base64,
},
];
} else {
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: media[1].split('?')[0],
},
];
payload.query = media[2] || content;
}
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: contentSplit[1].split('?')[0],
},
];
payload.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
@ -298,7 +246,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
await instance.client.sendPresenceUpdate('paused', remoteJid);
if (answer) {
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
}
await this.prismaRepository.integrationSession.update({

View File

@ -5,7 +5,6 @@ import { ConfigService, HttpServer } from '@config/env.config';
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
import { isURL } from 'class-validator';
import { v4 as uuidv4 } from 'uuid';
import { BaseChatbotService } from '../../base-chatbot.service';
@ -83,43 +82,23 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
// Handle image message if present
if (this.isImageMessage(content) && msg) {
const media = content.split('|');
parts[0].text = media[2] || content;
const contentSplit = content.split('|');
parts[0].text = contentSplit[2] || content;
try {
if (msg.message.mediaUrl || msg.message.base64) {
let mediaBase64 = msg.message.base64 || null;
// Download the image
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
const fileContent = Buffer.from(mediaBuffer).toString('base64');
const fileName = contentSplit[2] || `${msg.key?.id || 'image'}.jpg`;
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
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
const fileContent = Buffer.from(mediaBuffer).toString('base64');
const fileName = media[2] || `${msg.key?.id || 'image'}.jpg`;
parts.push({
type: 'file',
file: {
name: fileName,
mimeType: 'image/jpeg',
bytes: fileContent,
},
} as any);
}
parts.push({
type: 'file',
file: {
name: fileName,
mimeType: 'image/jpeg',
bytes: fileContent,
},
} as any);
} catch (fileErr) {
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
}
@ -195,7 +174,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
} catch (error) {
this.logger.error(

View File

@ -6,7 +6,6 @@ import { ConfigService, HttpServer } from '@config/env.config';
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { isURL } from 'class-validator';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
@ -72,26 +71,16 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
}
}
if (this.isImageMessage(content) && msg) {
const media = content.split('|');
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
if (msg.message.mediaUrl || msg.message.base64) {
payload.files = [
{
type: 'image',
url: msg.message.base64 || msg.message.mediaUrl,
},
];
} else {
payload.files = [
{
type: 'image',
url: media[1].split('?')[0],
},
];
}
payload.query = media[2] || content;
payload.files = [
{
type: 'image',
url: contentSplit[1].split('?')[0],
},
];
payload.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
@ -126,10 +115,15 @@ 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, {
headers,
});
this.logger.debug(`[EvolutionBot] Response received - Status: ${response.status}`);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
@ -140,6 +134,10 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
// Validate linkPreview is boolean and default to true for backward compatibility
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("'")) {
const innerContent = message.slice(1, -1);
if (!innerContent.includes("'")) {
@ -148,8 +146,17 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
}
if (message) {
// Use the base class method that handles splitMessages functionality
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, linkPreview);
// Send message directly with validated linkPreview option
await instance.textMessage(
{
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 {
this.logger.warn(`[EvolutionBot] No message content received from bot response`);
}

View File

@ -5,7 +5,6 @@ import { Integration } from '@api/types/wa.types';
import { ConfigService, HttpServer } from '@config/env.config';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { isURL } from 'class-validator';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
@ -83,28 +82,17 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
}
if (this.isImageMessage(content)) {
const media = content.split('|');
const contentSplit = content.split('|');
if (msg.message.mediaUrl || msg.message.base64) {
payload.uploads = [
{
data: msg.message.base64 || msg.message.mediaUrl,
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
} else {
payload.uploads = [
{
data: media[1].split('?')[0],
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
payload.question = media[2] || content;
}
payload.uploads = [
{
data: contentSplit[1].split('?')[0],
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
payload.question = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
@ -142,7 +130,7 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
if (message) {
// Use the base class method to send the message to WhatsApp
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
}

View File

@ -51,7 +51,6 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
pushName: pushName,
keyId: msg?.key?.id,
fromMe: msg?.key?.fromMe,
quotedMessage: msg?.contextInfo?.quotedMessage,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
@ -79,7 +78,7 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
const message = response?.data?.output || response?.data?.answer;
// Use base class method instead of custom implementation
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {

View File

@ -6,7 +6,6 @@ import { IntegrationSession, OpenaiBot, OpenaiSetting } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
import { isURL } from 'class-validator';
import FormData from 'form-data';
import OpenAI from 'openai';
import P from 'pino';
@ -86,7 +85,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
remoteJid,
"Sorry, I couldn't transcribe your audio message. Could you please type your message instead?",
settings,
true,
);
return;
}
@ -175,7 +173,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
}
// Process with the appropriate API based on bot type
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content, msg);
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content);
} catch (error) {
this.logger.error(`Error in process: ${error.message || JSON.stringify(error)}`);
return;
@ -193,7 +191,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
this.logger.log(`Sending message to bot for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
@ -225,11 +222,10 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
pushName,
false, // Not fromMe
content,
msg,
);
} else {
this.logger.log('Processing with ChatCompletion API');
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content, msg);
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content);
}
this.logger.log(`Got response from OpenAI: ${message?.substring(0, 50)}${message?.length > 50 ? '...' : ''}`);
@ -237,7 +233,7 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
// Send the response
if (message) {
this.logger.log('Sending message to WhatsApp');
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
} else {
this.logger.error('No message to send to WhatsApp');
}
@ -272,7 +268,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
pushName: string,
fromMe: boolean,
content: string,
msg?: any,
): Promise<string> {
const messageData: any = {
role: fromMe ? 'assistant' : 'user',
@ -281,35 +276,18 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
// Handle image messages
if (this.isImageMessage(content)) {
const media = content.split('|');
const contentSplit = 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 = [
{ type: 'text', text: media[2] || content },
{
type: 'image_url',
image_url: {
url: url,
},
messageData.content = [
{ type: 'text', text: contentSplit[2] || content },
{
type: 'image_url',
image_url: {
url: url,
},
];
}
},
];
}
// Get thread ID from session or create new thread
@ -398,7 +376,6 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
openaiBot: OpenaiBot,
remoteJid: string,
content: string,
msg?: any,
): Promise<string> {
this.logger.log('Starting processChatCompletionMessage');
@ -491,26 +468,18 @@ export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting>
// Handle image messages
if (this.isImageMessage(content)) {
this.logger.log('Found image message');
const media = content.split('|');
const contentSplit = 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 = [
{ type: 'text', text: media[2] || content },
{
type: 'image_url',
image_url: {
url: url,
},
messageData.content = [
{ type: 'text', text: contentSplit[2] || content },
{
type: 'image_url',
image_url: {
url: url,
},
];
}
},
];
}
// Combine all messages: system messages, pre-defined messages, conversation history, and current message

View File

@ -318,7 +318,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
} else if (formattedText.includes('[buttons]')) {
await this.processButtonMessage(instance, formattedText, session.remoteJid);
} else {
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings, true);
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
}
sendTelemetry('/message/sendText');
@ -327,7 +327,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (message.type === 'image') {
await instance.mediaMessage(
{
number: session.remoteJid,
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'image',
media: message.content.url,
@ -342,7 +342,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (message.type === 'video') {
await instance.mediaMessage(
{
number: session.remoteJid,
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'video',
media: message.content.url,
@ -357,7 +357,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (message.type === 'audio') {
await instance.audioWhatsapp(
{
number: session.remoteJid,
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
encoding: true,
audio: message.content.url,
@ -393,7 +393,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
} else if (formattedText.includes('[buttons]')) {
await this.processButtonMessage(instance, formattedText, session.remoteJid);
} else {
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings, true);
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
}
sendTelemetry('/message/sendText');
@ -441,7 +441,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
*/
private async processListMessage(instance: any, formattedText: string, remoteJid: string) {
const listJson = {
number: remoteJid,
number: remoteJid.split('@')[0],
title: '',
description: '',
buttonText: '',
@ -490,7 +490,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
*/
private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) {
const buttonJson = {
number: remoteJid,
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
@ -642,21 +642,15 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (!content) {
if (unknownMessage) {
await this.sendMessageWhatsApp(
waInstance,
remoteJid,
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
{
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
},
true,
);
});
sendTelemetry('/message/sendText');
}
return;
@ -807,21 +801,15 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (!data?.messages || data.messages.length === 0) {
if (!content) {
if (unknownMessage) {
await this.sendMessageWhatsApp(
waInstance,
remoteJid,
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
{
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
},
true,
);
});
sendTelemetry('/message/sendText');
}
return;
@ -915,21 +903,15 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (!content) {
if (unknownMessage) {
await this.sendMessageWhatsApp(
waInstance,
remoteJid,
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, {
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
{
delayMessage,
expire,
keywordFinish,
listeningFromMe,
stopBotFromMe,
keepOpen,
unknownMessage,
},
true,
);
});
sendTelemetry('/message/sendText');
}
return;

View File

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

View File

@ -40,11 +40,6 @@ export class EventDto {
useTLS?: boolean;
events?: string[];
};
kafka?: {
enabled?: boolean;
events?: string[];
};
}
export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
@ -87,10 +82,5 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
useTLS?: boolean;
events?: string[];
};
kafka?: {
enabled?: boolean;
events?: string[];
};
};
}

View File

@ -1,4 +1,3 @@
import { KafkaController } from '@api/integrations/event/kafka/kafka.controller';
import { NatsController } from '@api/integrations/event/nats/nats.controller';
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
@ -18,7 +17,6 @@ export class EventManager {
private natsController: NatsController;
private sqsController: SqsController;
private pusherController: PusherController;
private kafkaController: KafkaController;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
this.prisma = prismaRepository;
@ -30,7 +28,6 @@ export class EventManager {
this.nats = new NatsController(prismaRepository, waMonitor);
this.sqs = new SqsController(prismaRepository, waMonitor);
this.pusher = new PusherController(prismaRepository, waMonitor);
this.kafka = new KafkaController(prismaRepository, waMonitor);
}
public set prisma(prisma: PrismaRepository) {
@ -96,20 +93,12 @@ export class EventManager {
return this.pusherController;
}
public set kafka(kafka: KafkaController) {
this.kafkaController = kafka;
}
public get kafka() {
return this.kafkaController;
}
public init(httpServer: Server): void {
this.websocket.init(httpServer);
this.rabbitmq.init();
this.nats.init();
this.sqs.init();
this.pusher.init();
this.kafka.init();
}
public async emit(eventData: {
@ -123,7 +112,6 @@ export class EventManager {
apiKey?: string;
local?: boolean;
integration?: string[];
extra?: Record<string, any>;
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);
@ -131,47 +119,42 @@ export class EventManager {
await this.sqs.emit(eventData);
await this.webhook.emit(eventData);
await this.pusher.emit(eventData);
await this.kafka.emit(eventData);
}
public async setInstance(instanceName: string, data: any): Promise<any> {
if (data.websocket) {
if (data.websocket)
await this.websocket.set(instanceName, {
websocket: {
enabled: true,
events: data.websocket?.events,
},
});
}
if (data.rabbitmq) {
if (data.rabbitmq)
await this.rabbitmq.set(instanceName, {
rabbitmq: {
enabled: true,
events: data.rabbitmq?.events,
},
});
}
if (data.nats) {
if (data.nats)
await this.nats.set(instanceName, {
nats: {
enabled: true,
events: data.nats?.events,
},
});
}
if (data.sqs) {
if (data.sqs)
await this.sqs.set(instanceName, {
sqs: {
enabled: true,
events: data.sqs?.events,
},
});
}
if (data.webhook) {
if (data.webhook)
await this.webhook.set(instanceName, {
webhook: {
enabled: true,
@ -182,9 +165,8 @@ export class EventManager {
byEvents: data.webhook?.byEvents,
},
});
}
if (data.pusher) {
if (data.pusher)
await this.pusher.set(instanceName, {
pusher: {
enabled: true,
@ -196,15 +178,5 @@ export class EventManager {
useTLS: data.pusher?.useTLS,
},
});
}
if (data.kafka) {
await this.kafka.set(instanceName, {
kafka: {
enabled: true,
events: data.kafka?.events,
},
});
}
}
}

View File

@ -1,4 +1,3 @@
import { KafkaRouter } from '@api/integrations/event/kafka/kafka.router';
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
@ -19,6 +18,5 @@ export class EventRouter {
this.router.use('/nats', new NatsRouter(...guards).router);
this.router.use('/pusher', new PusherRouter(...guards).router);
this.router.use('/sqs', new SqsRouter(...guards).router);
this.router.use('/kafka', new KafkaRouter(...guards).router);
}
}

View File

@ -22,9 +22,6 @@ export const eventSchema: JSONSchema7 = {
sqs: {
$ref: '#/$defs/event',
},
kafka: {
$ref: '#/$defs/event',
},
},
$defs: {
event: {

View File

@ -1,416 +0,0 @@
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;
}
}
}

View File

@ -1,36 +0,0 @@
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();
}

View File

@ -1,21 +0,0 @@
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'],
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ const bucketExists = async () => {
try {
const list = await minioClient.listBuckets();
return list.find((bucket) => bucket.name === bucketName);
} catch {
} catch (error) {
return false;
}
}

View File

@ -48,14 +48,9 @@ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const metricsIPWhitelist = (req: Request, res: Response, next: NextFunction) => {
const metricsConfig = configService.get('METRICS');
const allowedIPs = metricsConfig.ALLOWED_IPS?.split(',').map((ip) => ip.trim()) || ['127.0.0.1'];
const clientIPs = [
req.ip,
req.connection.remoteAddress,
req.socket.remoteAddress,
req.headers['x-forwarded-for'],
].filter((ip) => ip !== undefined);
const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;
if (allowedIPs.filter((ip) => clientIPs.includes(ip)) === 0) {
if (!allowedIPs.includes(clientIP)) {
return res.status(403).send('Forbidden: IP not allowed');
}

View File

@ -1,11 +1,9 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { TemplateDeleteDto, TemplateDto, TemplateEditDto } from '@api/dto/template.dto';
import { TemplateDto } from '@api/dto/template.dto';
import { templateController } from '@api/server.module';
import { ConfigService } from '@config/env.config';
import { createMetaErrorResponse } from '@utils/errorResponse';
import { templateDeleteSchema } from '@validate/templateDelete.schema';
import { templateEditSchema } from '@validate/templateEdit.schema';
import { instanceSchema, templateSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
@ -37,38 +35,6 @@ export class TemplateRouter extends RouterBroker {
res.status(errorResponse.status).json(errorResponse);
}
})
.post(this.routerPath('edit'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<TemplateEditDto>({
request: req,
schema: templateEditSchema,
ClassRef: TemplateEditDto,
execute: (instance, data) => templateController.editTemplate(instance, data),
});
res.status(HttpStatus.OK).json(response);
} catch (error) {
console.error('Template edit error:', error);
const errorResponse = createMetaErrorResponse(error, 'template_edit');
res.status(errorResponse.status).json(errorResponse);
}
})
.delete(this.routerPath('delete'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<TemplateDeleteDto>({
request: req,
schema: templateDeleteSchema,
ClassRef: TemplateDeleteDto,
execute: (instance, data) => templateController.deleteTemplate(instance, data),
});
res.status(HttpStatus.OK).json(response);
} catch (error) {
console.error('Template delete error:', error);
const errorResponse = createMetaErrorResponse(error, 'template_delete');
res.status(errorResponse.status).json(errorResponse);
}
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<InstanceDto>({

View File

@ -82,7 +82,7 @@ const proxyService = new ProxyService(waMonitor);
export const proxyController = new ProxyController(proxyService, waMonitor);
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
export const chatwootController = new ChatwootController(chatwootService, configService);
export const chatwootController = new ChatwootController(chatwootService, configService, prismaRepository);
const settingsService = new SettingsService(waMonitor);
export const settingsController = new SettingsController(settingsService);

View File

@ -60,7 +60,6 @@ export class ChannelStartupService {
this.instance.number = instance.number;
this.instance.token = instance.token;
this.instance.businessId = instance.businessId;
this.instance.ownerJid = instance.ownerJid;
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
@ -432,13 +431,7 @@ export class ChannelStartupService {
return data;
}
public async sendDataWebhook<T extends object = any>(
event: Events,
data: T,
local = true,
integration?: string[],
extra?: Record<string, any>,
) {
public async sendDataWebhook<T extends object = any>(event: Events, data: T, local = true, integration?: string[]) {
const serverUrl = this.configService.get<HttpServer>('SERVER').URL;
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
@ -459,7 +452,6 @@ export class ChannelStartupService {
apiKey: expose && instanceApikey ? instanceApikey : null,
local,
integration,
extra,
});
}
@ -498,23 +490,20 @@ export class ChannelStartupService {
}
public async fetchContacts(query: Query<Contact>) {
const where: any = {
const remoteJid = query?.where?.remoteJid
? query?.where?.remoteJid.includes('@')
? query.where?.remoteJid
: createJid(query.where?.remoteJid)
: null;
const where = {
instanceId: this.instanceId,
};
if (query?.where?.remoteJid) {
const remoteJid = query.where.remoteJid.includes('@') ? query.where.remoteJid : createJid(query.where.remoteJid);
if (remoteJid) {
where['remoteJid'] = remoteJid;
}
if (query?.where?.id) {
where['id'] = query.where.id;
}
if (query?.where?.pushName) {
where['pushName'] = query.where.pushName;
}
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
where,
};
@ -543,12 +532,14 @@ export class ChannelStartupService {
public cleanMessageData(message: any) {
if (!message) return message;
const cleanedMessage = { ...message };
if (cleanedMessage.message) {
const { mediaUrl } = cleanedMessage.message;
delete cleanedMessage.message.base64;
const mediaUrl = cleanedMessage.message.mediaUrl;
delete cleanedMessage.message.base64;
if (cleanedMessage.message) {
// Limpa imageMessage
if (cleanedMessage.message.imageMessage) {
cleanedMessage.message.imageMessage = {
@ -590,10 +581,10 @@ export class ChannelStartupService {
name: cleanedMessage.message.documentWithCaptionMessage.name,
};
}
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
}
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
return cleanedMessage;
}
@ -835,7 +826,7 @@ export class ChannelStartupService {
const msg = message.message;
// Se só tem messageContextInfo, não é mídia válida
if (Object.keys(msg).length === 1 && Object.prototype.hasOwnProperty.call(msg, 'messageContextInfo')) {
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
return false;
}

View File

@ -38,37 +38,25 @@ export class WAMonitoringService {
private readonly logger = new Logger('WAMonitoringService');
public readonly waInstances: Record<string, any> = {};
private readonly delInstanceTimeouts: Record<string, NodeJS.Timeout> = {};
private readonly providerSession: ProviderSession;
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
// Clear previous timeout if exists
if (this.delInstanceTimeouts[instance]) {
clearTimeout(this.delInstanceTimeouts[instance]);
}
// Set new timeout and store reference
this.delInstanceTimeouts[instance] = setTimeout(
setTimeout(
async () => {
try {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
} finally {
// Clean up timeout reference
delete this.delInstanceTimeouts[instance];
}
},
1000 * 60 * time,
@ -76,13 +64,6 @@ export class WAMonitoringService {
}
}
public clearDelInstanceTime(instance: string) {
if (this.delInstanceTimeouts[instance]) {
clearTimeout(this.delInstanceTimeouts[instance]);
delete this.delInstanceTimeouts[instance];
}
}
public async instanceInfo(instanceNames?: string[]): Promise<any> {
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
@ -290,19 +271,9 @@ export class WAMonitoringService {
token: instanceData.token,
number: instanceData.number,
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();
} else {
this.logger.info(
`Skipping auto-connect for instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus || 'close'})`,
);
}
await instance.connectToWhatsapp();
this.waInstances[instanceData.instanceName] = instance;
}
@ -328,7 +299,6 @@ export class WAMonitoringService {
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
connectionStatus: instanceData.connectionStatus as any, // Pass connection status
};
this.setInstance(instance);
@ -357,8 +327,6 @@ export class WAMonitoringService {
token: instance.token,
number: instance.number,
businessId: instance.businessId,
ownerJid: instance.ownerJid,
connectionStatus: instance.connectionStatus as any, // Pass connection status
});
}),
);
@ -383,7 +351,6 @@ export class WAMonitoringService {
integration: instance.integration,
token: instance.token,
businessId: instance.businessId,
connectionStatus: instance.connectionStatus as any, // Pass connection status
});
}),
);
@ -394,8 +361,6 @@ export class WAMonitoringService {
try {
await this.waInstances[instanceName]?.sendDataWebhook(Events.REMOVE_INSTANCE, null);
this.clearDelInstanceTime(instanceName);
this.cleaningUp(instanceName);
this.cleaningStoreData(instanceName);
} finally {
@ -412,8 +377,6 @@ export class WAMonitoringService {
try {
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
this.clearDelInstanceTime(instanceName);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
this.waInstances[instanceName]?.clearCacheChatwoot();
}

View File

@ -25,7 +25,7 @@ export class ProxyService {
}
return result;
} catch {
} catch (error) {
return null;
}
}

View File

@ -24,7 +24,7 @@ export class SettingsService {
}
return result;
} catch {
} catch (error) {
return null;
}
}

View File

@ -88,77 +88,6 @@ export class TemplateService {
}
}
public async edit(
instance: InstanceDto,
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
) {
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
if (!getInstance) {
throw new Error('Instance not found');
}
this.businessId = getInstance.businessId;
this.token = getInstance.token;
const payload: Record<string, unknown> = {};
if (typeof data.category === 'string') payload.category = data.category;
if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
if (data.components) payload.components = data.components;
const response = await this.requestEditTemplate(data.templateId, payload);
if (!response || response.error) {
if (response && response.error) {
const metaError = new Error(response.error.message || 'WhatsApp API Error');
(metaError as any).template = response.error;
throw metaError;
}
throw new Error('Error to edit template');
}
return response;
}
public async delete(instance: InstanceDto, data: { name: string; hsmId?: string }) {
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
if (!getInstance) {
throw new Error('Instance not found');
}
this.businessId = getInstance.businessId;
this.token = getInstance.token;
const response = await this.requestDeleteTemplate({ name: data.name, hsm_id: data.hsmId });
if (!response || response.error) {
if (response && response.error) {
const metaError = new Error(response.error.message || 'WhatsApp API Error');
(metaError as any).template = response.error;
throw metaError;
}
throw new Error('Error to delete template');
}
try {
// Best-effort local cleanup of stored template metadata
await this.prismaRepository.template.deleteMany({
where: {
OR: [
{ name: data.name, instanceId: getInstance.id },
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
].filter(Boolean) as any,
},
});
} catch (err) {
this.logger.warn(
`Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}`,
);
}
return response;
}
private async requestTemplate(data: any, method: string) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
@ -187,38 +116,4 @@ export class TemplateService {
throw new Error(`Connection error: ${e.message}`);
}
}
private async requestEditTemplate(templateId: string, data: any) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${templateId}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.post(urlServer, data, { headers });
return result.data;
} catch (e) {
this.logger.error(
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
);
if (e.response?.data) return e.response.data;
throw new Error(`Connection error: ${e.message}`);
}
}
private async requestDeleteTemplate(params: { name: string; hsm_id?: string }) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`;
const headers = { Authorization: `Bearer ${this.token}` };
const result = await axios.delete(urlServer, { headers, params });
return result.data;
} catch (e) {
this.logger.error(
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
);
if (e.response?.data) return e.response.data;
throw new Error(`Connection error: ${e.message}`);
}
}
}

View File

@ -52,7 +52,6 @@ export declare namespace wa {
pairingCode?: string;
authState?: { state: AuthenticationState; saveCreds: () => void };
name?: string;
ownerJid?: string;
wuid?: string;
profileName?: string;
profilePictureUrl?: string;

View File

@ -153,34 +153,6 @@ 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 = {
ENABLED: boolean;
GLOBAL_EVENTS: boolean;
@ -400,7 +372,6 @@ export interface Env {
RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs;
KAFKA: Kafka;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
LOG: Log;
@ -616,68 +587,6 @@ export class ConfigService {
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: {
ENABLED: process.env?.WEBSOCKET_ENABLED === 'true',
GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true',

View File

@ -26,8 +26,8 @@ import cors from 'cors';
import express, { json, NextFunction, Request, Response, urlencoded } from 'express';
import { join } from 'path';
async function initWA() {
await waMonitor.loadInstance();
function initWA() {
waMonitor.loadInstance();
}
async function bootstrap() {
@ -159,9 +159,7 @@ async function bootstrap() {
server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT));
initWA().catch((error) => {
logger.error('Error loading instances: ' + error);
});
initWA();
onUnexpectedError();
}

View File

@ -3,6 +3,8 @@ import fs from 'fs';
import i18next from 'i18next';
import path from 'path';
const __dirname = path.resolve(process.cwd(), 'src', 'utils');
const languages = ['en', 'pt-BR', 'es'];
const translationsPath = path.join(__dirname, 'translations');
const configService: ConfigService = new ConfigService();

View File

@ -1,7 +1,5 @@
import { socksDispatcher } from 'fetch-socks';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { ProxyAgent } from 'undici';
type Proxy = {
host: string;
@ -19,23 +17,12 @@ function selectProxyAgent(proxyUrl: string): HttpsProxyAgent<string> | SocksProx
// the end so, we add the protocol constants without the `:` to avoid confusion.
const PROXY_HTTP_PROTOCOL = 'http:';
const PROXY_SOCKS_PROTOCOL = 'socks:';
const PROXY_SOCKS5_PROTOCOL = 'socks5:';
switch (url.protocol) {
case PROXY_HTTP_PROTOCOL:
return new HttpsProxyAgent(url);
case PROXY_SOCKS_PROTOCOL:
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);
}
return new SocksProxyAgent(url);
default:
throw new Error(`Unsupported proxy protocol: ${url.protocol}`);
}
@ -55,57 +42,3 @@ export function makeProxyAgent(proxy: Proxy | string): HttpsProxyAgent<string> |
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}`);
}
}

View File

@ -1,10 +1,7 @@
import { prismaRepository } from '@api/server.module';
import { configService, Database } from '@config/env.config';
import { Logger } from '@config/logger.config';
import dayjs from 'dayjs';
const logger = new Logger('OnWhatsappCache');
function getAvailableNumbers(remoteJid: string) {
const numbersAvailable: string[] = [];
@ -14,11 +11,6 @@ function getAvailableNumbers(remoteJid: string) {
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
if (remoteJid.startsWith('55')) {
const numberWithDigit =
@ -55,128 +47,36 @@ function getAvailableNumbers(remoteJid: string) {
numbersAvailable.push(remoteJid);
}
// TODO: Adiciona @domain apenas para números que não são @lid
return numbersAvailable.map((number) => `${number}@${domain}`);
}
interface ISaveOnWhatsappCacheParams {
remoteJid: 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;
lid?: string;
}
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
if (!configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
return;
}
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
const upsertsQuery = data.map((item) => {
const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
const numbersAvailable = getAvailableNumbers(remoteJid);
// 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
],
return prismaRepository.isOnWhatsapp.upsert({
create: {
remoteJid: remoteJid,
jidOptions: numbersAvailable.join(','),
lid: item.lid,
},
update: {
jidOptions: numbersAvailable.join(','),
lid: item.lid,
},
where: { remoteJid: remoteJid },
});
});
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);
await prismaRepository.$transaction(upsertsQuery);
}
}
export async function getOnWhatsappCache(remoteJids: string[]) {

View File

@ -1,7 +1,6 @@
import { prismaRepository } from '@api/server.module';
import { CacheService } from '@api/services/cache.service';
import { CacheConf, configService } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR } from '@config/path.config';
import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys';
import fs from 'fs/promises';
@ -20,7 +19,7 @@ export async function keyExists(sessionId: string): Promise<any> {
try {
const key = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
return !!key;
} catch {
} catch (error) {
return false;
}
}
@ -39,7 +38,7 @@ export async function saveKey(sessionId: string, keyJson: any): Promise<any> {
where: { sessionId: sessionId },
data: { creds: JSON.stringify(keyJson) },
});
} catch {
} catch (error) {
return null;
}
}
@ -50,7 +49,7 @@ export async function getAuthKey(sessionId: string): Promise<any> {
if (!register) return null;
const auth = await prismaRepository.session.findUnique({ where: { sessionId: sessionId } });
return JSON.parse(auth?.creds);
} catch {
} catch (error) {
return null;
}
}
@ -60,7 +59,7 @@ async function deleteAuthKey(sessionId: string): Promise<any> {
const register = await keyExists(sessionId);
if (!register) return;
await prismaRepository.session.delete({ where: { sessionId: sessionId } });
} catch {
} catch (error) {
return;
}
}
@ -69,20 +68,17 @@ async function fileExists(file: string): Promise<any> {
try {
const stat = await fs.stat(file);
if (stat.isFile()) return true;
} catch {
} catch (error) {
return;
}
}
const logger = new Logger('useMultiFileAuthStatePrisma');
export default async function useMultiFileAuthStatePrisma(
sessionId: string,
cache: CacheService,
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
removeCreds: () => Promise<void>;
}> {
const localFolder = path.join(INSTANCE_DIR, sessionId);
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
@ -123,7 +119,7 @@ export default async function useMultiFileAuthStatePrisma(
const parsedData = JSON.parse(rawData, BufferJSON.reviver);
return parsedData;
} catch {
} catch (error) {
return null;
}
}
@ -141,31 +137,11 @@ export default async function useMultiFileAuthStatePrisma(
} else {
await deleteAuthKey(sessionId);
}
} catch {
} catch (error) {
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');
if (!creds) {
creds = initAuthCreds();
@ -207,7 +183,5 @@ export default async function useMultiFileAuthStatePrisma(
saveCreds: () => {
return writeData(creds, 'creds');
},
removeCreds,
};
}

View File

@ -39,11 +39,7 @@ import { Logger } from '@config/logger.config';
import { AuthenticationCreds, AuthenticationState, BufferJSON, initAuthCreds, proto, SignalDataTypeMap } from 'baileys';
import { isNotEmpty } from 'class-validator';
export type AuthState = {
state: AuthenticationState;
saveCreds: () => Promise<void>;
removeCreds: () => Promise<void>;
};
export type AuthState = { state: AuthenticationState; saveCreds: () => Promise<void> };
export class AuthStateProvider {
constructor(private readonly providerFiles: ProviderFiles) {}
@ -90,18 +86,6 @@ export class AuthStateProvider {
return response;
};
const removeCreds = async () => {
const [response, error] = await this.providerFiles.removeSession(instance);
if (error) {
// this.logger.error(['removeData', error?.message, error?.stack]);
return;
}
logger.info({ action: 'remove.session', instance, response });
return;
};
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
return {
@ -142,10 +126,6 @@ export class AuthStateProvider {
saveCreds: async () => {
return await writeData(creds, 'creds');
},
removeCreds,
};
}
}
const logger = new Logger('useMultiFileAuthStatePrisma');

View File

@ -8,7 +8,6 @@ export async function useMultiFileAuthStateRedisDb(
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
removeCreds: () => Promise<void>;
}> {
const logger = new Logger('useMultiFileAuthStateRedisDb');
@ -37,16 +36,6 @@ export async function useMultiFileAuthStateRedisDb(
}
};
async function removeCreds(): Promise<any> {
try {
logger.warn({ action: 'redis.delete', instanceName });
return await cache.delete(instanceName);
} catch {
return;
}
}
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
return {
@ -87,7 +76,5 @@ export async function useMultiFileAuthStateRedisDb(
saveCreds: async () => {
return await writeData(creds, 'creds');
},
removeCreds,
};
}

View File

@ -195,9 +195,8 @@ export const contactValidateSchema: JSONSchema7 = {
_id: { type: 'string', minLength: 1 },
pushName: { type: 'string', minLength: 1 },
id: { type: 'string', minLength: 1 },
remoteJid: { type: 'string', minLength: 1 },
},
...isNotEmpty('_id', 'id', 'pushName', 'remoteJid'),
...isNotEmpty('_id', 'id', 'pushName'),
},
},
};

View File

@ -1,32 +0,0 @@
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'),
};

View File

@ -1,35 +0,0 @@
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'),
};

View File

@ -8,7 +8,5 @@ export * from './message.schema';
export * from './proxy.schema';
export * from './settings.schema';
export * from './template.schema';
export * from './templateDelete.schema';
export * from './templateEdit.schema';
export * from '@api/integrations/chatbot/chatbot.schema';
export * from '@api/integrations/event/event.schema';