Compare commits

...

61 Commits
2.3.4 ... 2.3.5

Author SHA1 Message Date
Davidson Gomes
d48fbc3a4e Merge branch 'release/2.3.5'
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
2025-10-15 09:58:37 -03:00
Davidson Gomes
cdf06666a1 chore(workflows): update checkout step to include submodules
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
- Added 'submodules: recursive' option to the checkout step in multiple workflow files to ensure submodules are properly initialized during CI/CD processes.
2025-10-15 09:58:27 -03:00
Davidson Gomes
5254928887 Merge branch 'release/2.3.5' 2025-10-15 09:48:00 -03:00
Davidson Gomes
8468690d37 chore(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 for the evolution-manager-v2.
- Replaced old JavaScript asset file with a new version for improved performance.
- Added a new CSS file for consistent styling across the application.
2025-10-15 09:47:32 -03:00
Willian Coqueiro
bdd9257c47 Merge branch 'develop' of https://github.com/KokeroO/evolution-api into develop 2025-10-15 09:47:32 -03:00
Willian Coqueiro
d6834c8741 fix(chatwoot): correct chatId extraction for non-group JIDs 2025-10-15 09:47:31 -03:00
Davidson Gomes
164beddb39 chore(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 for the evolution-manager-v2.
- Replaced old JavaScript asset file with a new version for improved performance.
- Added a new CSS file for consistent styling across the application.
2025-10-15 09:44:15 -03:00
Davidson Gomes
4991f1dc37 feat(telemetry): add message type telemetry logging in channel services
- Integrated telemetry logging for received messages in Evolution, WhatsApp Business, and Baileys services.
- Enhanced message tracking by sending the message type to the telemetry system for better observability.
2025-10-15 09:42:45 -03:00
Davidson Gomes
1b1e3b3e9d chore(changelog): update CHANGELOG for version 2.3.5 release date
- Updated the release date for version 2.3.5 to 2025-10-15.
- Adjusted subproject reference in evolution-manager-v2 to the latest commit.
2025-10-15 09:42:44 -03:00
Davidson Gomes
563ca2dd22 chore(changelog): update CHANGELOG for version 2.3.5
- Added features for Chatwoot enhancements, participants data handling, and LID to phone number conversion.
- Updated Docker configurations to include Kafka and frontend services.
- Fixed PostgreSQL migration errors and improved message handling in Baileys and Chatwoot services.
- Refactored TypeScript build process and implemented exponential backoff patterns.
2025-10-15 09:42:44 -03:00
Davidson Gomes
4e44bfb222 chore(manager): update asset files and dependencies
- Updated subproject reference in evolution-manager-v2.
- Replaced old JavaScript and CSS asset files with new versions for improved performance and styling.
- Added new CSS file for consistent font styling across the application.
- Updated the evolution logo image to the latest version.
2025-10-15 09:42:44 -03:00
Davidson Gomes
9edd600513 Merge pull request #2083 from davidmnzs/main
fix: correct the error of hardcoded prisma/kafka schema
2025-10-15 09:40:15 -03:00
Davidson Gomes
501b06d133 Merge branch 'release/2.3.5' 2025-10-15 09:38:34 -03:00
Davidson Gomes
dc530285d5 feat(telemetry): add message type telemetry logging in channel services
- Integrated telemetry logging for received messages in Evolution, WhatsApp Business, and Baileys services.
- Enhanced message tracking by sending the message type to the telemetry system for better observability.
2025-10-15 09:38:06 -03:00
Davidson Gomes
8775cdf036 chore(changelog): update CHANGELOG for version 2.3.5 release date
- Updated the release date for version 2.3.5 to 2025-10-15.
- Adjusted subproject reference in evolution-manager-v2 to the latest commit.
2025-10-15 09:32:09 -03:00
Davidson Gomes
6ad33df879 chore(changelog): update CHANGELOG for version 2.3.5
- Added features for Chatwoot enhancements, participants data handling, and LID to phone number conversion.
- Updated Docker configurations to include Kafka and frontend services.
- Fixed PostgreSQL migration errors and improved message handling in Baileys and Chatwoot services.
- Refactored TypeScript build process and implemented exponential backoff patterns.
2025-10-15 09:31:45 -03:00
Davidson Gomes
633d0b4c45 Merge pull request #2085 from KokeroO/develop
Convert LIDs to PN by sending a call rejection message
2025-10-15 09:25:37 -03:00
Davidson Gomes
82c0eadf7c chore(manager): update asset files and dependencies
- Updated subproject reference in evolution-manager-v2.
- Replaced old JavaScript and CSS asset files with new versions for improved performance and styling.
- Added new CSS file for consistent font styling across the application.
- Updated the evolution logo image to the latest version.
2025-10-15 09:25:21 -03:00
Willian Coqueiro
1756abf1e6 Merge branch 'develop' of https://github.com/KokeroO/evolution-api into develop 2025-10-14 05:33:54 +00:00
Willian Coqueiro
a2f48030dc Merge branch 'develop' of https://github.com/KokeroO/evolution-api into develop 2025-10-14 05:33:33 +00:00
Willian Coqueiro
3214a9fb5b fix(chatwoot): correct chatId extraction for non-group JIDs 2025-10-14 05:25:36 +00:00
Willian Coqueiro
4b89e3f987 fix(chatwoot): correct chatId extraction for non-group JIDs 2025-10-14 02:16:22 +00:00
Willian Coqueiro
72622dca31 Merge upstream/develop into develop 2025-10-14 02:12:15 +00:00
davidmnzs
d73b72b67e fix: correct the error of hardcoded prisma/kafka schema 2025-10-13 20:28:17 -03:00
Davidson Gomes
20eef33df3 Merge pull request #2076 from KokeroO/fix/chatwoot
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
Implementations and corrections of previous commits in the chatwoot and baileys services
2025-10-13 12:19:58 -03:00
Davidson Gomes
37571c03b4 Merge pull request #2072 from nolramaf/fix/media-content-validation
fix/media content validation
2025-10-13 12:19:05 -03:00
Willian Coqueiro
017949458b refactor(baileys): simplify linkPreview handling in BaileysStartupService 2025-10-12 15:38:05 +00:00
Willian Coqueiro
2feaf1c74e fix(baileys): handle undefined status in update by defaulting to 'DELETED' 2025-10-12 15:29:48 +00:00
Willian Coqueiro
4b043cb4b8 refactor: update TypeScript build process and dependencies
- Changed the build command in package.json to use TypeScript compiler (tsc) with noEmit option.
- Added @swc/core and @swc/helpers as development dependencies for improved performance.

refactor: clean up WhatsApp Baileys service

- Removed unused properties and interfaces related to message keys.
- Simplified message handling logic by removing redundant checks and conditions.
- Updated message timestamp handling for consistency.
- Improved readability and maintainability by restructuring code and removing commented-out sections.

refactor: optimize Chatwoot service

- Streamlined database queries by reusing PostgreSQL client connection.
- Enhanced conversation creation logic with better cache handling.
- Removed unnecessary methods and improved existing ones for clarity.
- Updated message sending logic to handle file streams instead of buffers.

fix: improve translation loading mechanism

- Simplified translation file loading by removing environment variable checks.
- Ensured translations are loaded from a consistent path within the project structure.
2025-10-12 15:03:48 +00:00
Marlon Alves
b0d261b305 fix/media content validation 2025-10-11 04:13:12 -03:00
Willian Coqueiro
c041986e26 Merge upstream/develop into develop 2025-10-10 02:11:44 +00:00
Davidson Gomes
0976109d27 Merge pull request #2025 from guispiller/main
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
feat: convert LID to phoneNumber on GROUP_PARTICIPANTS_UPDATE webhook
2025-10-09 15:05:41 -03:00
Davidson Gomes
b808dda33b Merge pull request #2048 from dersonbsb2022/main
feat(chatwoot): comprehensive improvements to message handling, editing, deletion and i18n (translate messages)
2025-10-09 14:59:53 -03:00
Anderson Silva
98b7f15a43 fix(baileys): update to 7.0.0-rc.5 and fix assertSessions signature
Problem:
- GitHub Actions failing: Expected 1 arguments, but got 2
- Local had outdated Baileys 7.0.0-rc.3 in node_modules
- assertSessions signature changed between versions

Solution:
- Fresh npm install with Baileys 7.0.0-rc.5
- Updated assertSessions to pass only jids (no force param)
- Regenerated Prisma Client after reinstall
- Updated package-lock.json for version consistency

Changes:
- assertSessions now receives 1 argument (jids only)
- Kept force param in method signature for API compatibility
- Removed @ts-expect-error directives (no longer needed)

Tested:
-  Server starts successfully
-  Build passes without errors
-  Lint passes
2025-10-06 19:30:13 -03:00
Anderson Silva
94ddc0dfbe fix(baileys): use type assertion for assertSessions compatibility
Problem:
- GitHub Actions shows: Expected 1 arguments, but got 2
- Local environment shows: Expected 2 arguments, but got 1
- Different Baileys versions/definitions between environments

Solution:
- Use 'as any' type assertion for force parameter
- Maintains compatibility with both signature variations
- Allows code to work in all environments

Technical notes:
- Local: baileys@7.0.0-rc.5 expects 2 arguments (jids, force)
- GitHub Actions: May have different version/cache expecting 1 argument
- Type assertion bypasses strict type checking for cross-version compatibility
2025-10-06 19:12:32 -03:00
Anderson Silva
d4b0cfd2ba fix(chatwoot): resolve webhook timeout on deletion with 5+ images
Problem:
- Chatwoot shows red error when deleting messages with 5+ images
- Cause: Chatwoot webhook timeout of 5 seconds
- Processing 5 images takes ~9 seconds
- Duplicate webhooks arrive during processing

Solution:
- Implemented async processing with setImmediate()
- Webhook responds immediately (< 100ms)
- Deletion processes in background without blocking
- Maintains idempotency with cache (1 hour TTL)
- Maintains lock mechanism (60 seconds TTL)

Benefits:
- Scales infinitely (10, 20, 100+ images)
- No timeout regardless of quantity
- No error messages in Chatwoot
- Reliable background processing

Tested:
- 5 images: 9s background processing
- Webhook response: < 100ms
- No red error in Chatwoot
- Deletion completes successfully

BREAKING CHANGE: Fixed assertSessions signature to accept force parameter
2025-10-06 16:14:26 -03:00
dersonbsb2022
a5a46dc72a Merge branch 'develop' into main 2025-10-06 15:21:10 -03:00
Anderson Silva
e13434804c refactor: implement exponential backoff patterns and extract magic numbers to constants
- Extract HTTP timeout constant (60s for large file downloads)
- Extract S3/MinIO retry configuration (3 retries, 1s-8s exponential backoff)
- Extract database polling retry configuration (5 retries, 100ms-2s exponential backoff)
- Extract webhook and lock polling delays to named constants
- Extract cache TTL values (5min for messages, 30min for updates) in Baileys service
- Implement exponential backoff for S3/MinIO downloads following webhook controller pattern
- Implement exponential backoff for database polling removing fixed delays
- Add deletion event lock to prevent race conditions with duplicate webhooks
- Process deletion events immediately (no delay) to fix Chatwoot local storage red error
- Make i18n translations path configurable via TRANSLATIONS_BASE_DIR env variable
- Add detailed logging for deletion events debugging

Addresses code review suggestions from Sourcery AI and Copilot AI:
- Magic numbers extracted to well-documented constants
- Retry configurations consolidated and clearly separated by use case
- S3/MinIO retry uses longer delays (external storage)
- Database polling uses shorter delays (internal operations)
- Fixes Chatwoot local storage deletion error (red message issue)
- Maintains full compatibility with S3/MinIO storage (tested)

Breaking changes: None - all changes are internal improvements
2025-10-06 15:10:38 -03:00
Davidson Gomes
53cd7d5d13 chore(deps): update baileys package to version 7.0.0-rc.5
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
- Bumped baileys dependency version in package.json and package-lock.json to 7.0.0-rc.5 for improved functionality and bug fixes.
- Added p-queue and p-timeout packages for enhanced performance and timeout management.
2025-10-06 14:29:22 -03:00
Spiller
bedfb019aa fix lint 2025-10-06 11:53:50 -03:00
Anderson Silva
6e1d027750 feat(chatwoot): comprehensive improvements to message handling, editing, deletion and i18n
- Fix bidirectional message deletion between Chatwoot and WhatsApp
- Support deletion of multiple attachments sent together
- Implement proper message editing with 'Edited Message:' prefix format
- Enable deletion of edited messages by updating chatwootMessageId
- Skip cache for deleted messages (messageStubType === 1) to prevent duplicates
- Fix i18n translation path detection for production environment
- Add automatic dev/prod path resolution for translation files
- Improve error handling and logging for message operations

Technical improvements:
- Changed Chatwoot deletion query from findFirst to findMany for multiple attachments
- Fixed instanceId override issue in message deletion payload
- Added retry logic with Prisma MessageUpdate validation
- Implemented cache bypass for revoked messages to ensure proper processing
- Enhanced i18n to detect dist/ folder in production vs src/ in development

Resolves issues with:
- Message deletion not working from Chatwoot to WhatsApp
- Multiple attachments causing incomplete deletion
- Edited messages showing raw i18n keys instead of translated text
- Cache collision preventing deletion of edited messages
- Production environment not loading translation files correctly

Note: Tested and validated with Chatwoot v4.1 in production environment
2025-10-03 14:47:24 -03:00
Spiller
fb1fa4d91a feat: add participantsData field maintaining backward
compatibility

  - Keep original participants array (string[]) for backward
  compatibility
  - Add new participantsData field with resolved phone numbers and
  metadata
  - Consumers can migrate gradually from participants to
  participantsData
  - No breaking changes to existing webhook integrations

  Payload structure:
  - participants: string[] (original JID strings)
  - participantsData: object[] (enhanced with phoneNumber, name,
  imgUrl)
2025-09-30 10:12:14 -03:00
Spiller
57ea6707bc feat: convert LID to phoneNumber on
GROUP_PARTICIPANTS_UPDATE
2025-09-29 20:50:39 -03:00
Davidson Gomes
ad8df44236 Merge pull request #2023 from Vitordotpy/fix/chatwoot-conversation-handling
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
fix(chatwoot): Corrige Reabertura de Conversas e Loop de Mensagem de Conexão
2025-09-29 16:08:52 -03:00
Vitordotpy
c132379b3a fix(chatwoot): ajustar lógica de verificação de conversas e cache
Este commit modifica a lógica de verificação de conversas no serviço Chatwoot, garantindo que a busca por conversas ativas seja priorizada em relação ao uso de cache. A verificação de cache foi removida em pontos críticos para evitar que conversas desatualizadas sejam utilizadas, melhorando a precisão na recuperação de dados. Além disso, a lógica de reabertura de conversas foi refinada para garantir que as interações sejam tratadas corretamente, mantendo a experiência do usuário mais fluida.
2025-09-29 15:26:24 -03:00
Vitordotpy
f7862637b1 fix(chatwoot): otimizar lógica de reabertura de conversas e notificação de conexão
Este commit introduz melhorias na integração com o Chatwoot, focando na reabertura de conversas e na notificação de conexão. A lógica foi refatorada para centralizar a busca por conversas abertas e a reabertura de conversas resolvidas, garantindo que interações não sejam perdidas. Além disso, foi implementado um intervalo mínimo para notificações de conexão, evitando mensagens excessivas e melhorando a experiência do usuário.
2025-09-28 22:38:45 -03:00
Vitordotpy
0d8e8bc0fb fix(chatwoot): corrige reabertura de conversas e loop de conexão
Este commit aborda duas questões críticas na integração com o Chatwoot para melhorar a estabilidade e a experiência do agente.

Primeiro, as conversas que já estavam marcadas como "resolvidas" no Chatwoot não eram reabertas automaticamente quando o cliente enviava uma nova mensagem. Isso foi corrigido para que o sistema verifique o status da conversa e a reabra, garantindo que nenhuma nova interação seja perdida.

Segundo, um bug no tratamento do evento de conexão fazia com que a mensagem de status "Conexão estabelecida com sucesso" fosse enviada repetidamente, poluindo o histórico da conversa. A lógica foi ajustada para garantir que esta notificação seja enviada apenas uma vez por evento de conexão.
2025-09-28 22:19:36 -03:00
Davidson Gomes
b62917e80f Merge pull request #2021 from Vitordotpy/fix/message-update-and-i18n-errors
Some checks failed
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
fix(baileys): message update and i18n errors
2025-09-26 16:37:24 -03:00
Vitordotpy
eeb324227b fix(baileys): adicionar log de aviso para mensagens não encontradas
- Implementada uma mensagem de aviso no serviço Baileys quando a mensagem original não é encontrada durante a atualização, melhorando a rastreabilidade de erros.
- Ajustada a lógica de verificação do caminho de traduções para garantir que o diretório correto seja utilizado, com tratamento de erro caso não seja encontrado.
2025-09-26 16:12:40 -03:00
Vitordotpy
c31b62fb3d fix(baileys): corrigir verificação de mensagem no serviço Baileys
- Ajustada a lógica de verificação para garantir que o ID da mensagem seja definido apenas quando disponível, evitando possíveis erros de referência.
- Atualizada a definição do caminho de traduções para suportar a estrutura de diretórios em produção.
2025-09-26 16:00:39 -03:00
Davidson Gomes
22465c0a56 fix: corrigido incompatibilidade no use voise call da wavoip com versao nova da baileys 2025-09-26 13:00:52 -03:00
Davidson Gomes
da6f1bd540 chore(changelog): update CHANGELOG for Baileys v7.0.0-rc.4 and PostgreSQL connection improvements
- Added entry for Baileys version update to v7.0.0-rc.4.
- Refactored PostgreSQL connection handling and enhanced message processing capabilities.
2025-09-26 12:58:36 -03:00
Davidson Gomes
069786b9fe chore(deps): update baileys package to version 7.0.0-rc.4
- Bumped baileys dependency version in package.json and package-lock.json to 7.0.0-rc.4 for improved functionality and bug fixes.
2025-09-26 12:56:40 -03:00
Davidson Gomes
bd0c43feac Merge pull request #2017 from Vitordotpy/fix/enhanced-chatwoot-database-connection
Some checks are pending
Check Code Quality / check-lint-and-build (push) Waiting to run
Build Docker image / Build and Deploy (push) Waiting to run
Security Scan / CodeQL Analysis (javascript) (push) Waiting to run
Security Scan / Dependency Review (push) Waiting to run
Fix Chatwoot DB Connection Instability and Implement Stale Conversation Cache Handling
2025-09-26 07:35:26 -03:00
Vitordotpy
5dc1d02d0a refactor(chatbot): melhorar tratamento de erros em mensagens no Chatwoot
- Implementada a função `handleStaleConversationError` para centralizar a lógica de tratamento de erros relacionados a conversas não encontradas.
- A lógica de retry foi aprimorada para as funções `createMessage` e `sendData`, garantindo que as operações sejam reprocessadas corretamente em caso de falhas.
- Removido código duplicado e melhorada a legibilidade do serviço Chatwoot.
2025-09-25 17:38:10 -03:00
Vitor Manoel Santos Moura
8697329f71 Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
aplicação de desestruturação de objetos que é uma boa prática do ts

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-09-25 17:30:43 -03:00
Vitor Manoel Santos Moura
58b5561f72 Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
aplicação de desestruturação de objetos que é uma boa prática do ts

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-09-25 17:30:30 -03:00
Vitordotpy
093515555d refactor(chatbot): refatorar conexão com PostgreSQL e melhorar tratamento de mensagens
- Alterado método de obtenção da conexão PostgreSQL para ser assíncrono, melhorando a gestão de conexões.
- Implementada lógica de retry para criação de mensagens e conversas, garantindo maior robustez em caso de falhas.
- Ajustadas chamadas de consulta ao banco de dados para utilizar a nova abordagem de conexão.
- Adicionada nova propriedade `messageBodyForRetry` para facilitar o reenvio de mensagens em caso de erro.
2025-09-25 17:08:40 -03:00
Davidson Gomes
d8268b0eb1 fix(migration): resolve PostgreSQL migration error for Kafka integration
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
- Corrected table reference in migration SQL to align with naming conventions.
- Fixed foreign key constraint issue that caused migration failure.
- Ensured successful setup of Kafka integration by addressing database migration errors.
2025-09-24 13:59:23 -03:00
Davidson Gomes
4585850741 chore(release): bump version to 2.3.5 and update bug report template
Some checks are pending
Check Code Quality / check-lint-and-build (push) Waiting to run
Build Docker image / Build and Deploy (push) Waiting to run
Security Scan / CodeQL Analysis (javascript) (push) Waiting to run
Security Scan / Dependency Review (push) Waiting to run
- Updated package and lock files to version 2.3.5.
- Modified bug report template to reflect the new version number.
- Removed outdated Kafka Docker README file.
2025-09-23 18:42:07 -03:00
Davidson Gomes
6c150eed6d chore(docker): add Kafka and frontend services to Docker configurations
- Introduced Kafka and Zookeeper services in a new docker-compose file for better message handling.
- Added frontend service to both development and production docker-compose files for improved UI management.
- Updated evolution-manager-v2 submodule to the latest commit.
- Updated CHANGELOG for version 2.3.5 release.
2025-09-23 18:40:19 -03:00
30 changed files with 3824 additions and 3908 deletions

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.4]
- Evolution API version: [e.g. 2.3.5]
- Database: [e.g. PostgreSQL 14, MySQL 8.0]
- Connection type: [e.g. Baileys, WhatsApp Business API]
validations:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,44 @@
# 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

View File

@@ -0,0 +1,51 @@
version: '3.3'
services:
zookeeper:
container_name: zookeeper
image: confluentinc/cp-zookeeper:7.5.0
environment:
- ZOOKEEPER_CLIENT_PORT=2181
- ZOOKEEPER_TICK_TIME=2000
- ZOOKEEPER_SYNC_LIMIT=2
volumes:
- zookeeper_data:/var/lib/zookeeper/
ports:
- 2181:2181
kafka:
container_name: kafka
image: confluentinc/cp-kafka:7.5.0
depends_on:
- zookeeper
environment:
- KAFKA_BROKER_ID=1
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,OUTSIDE:PLAINTEXT
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,OUTSIDE://host.docker.internal:9094
- KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
- KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1
- KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1
- KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0
- KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
- KAFKA_LOG_RETENTION_HOURS=168
- KAFKA_LOG_SEGMENT_BYTES=1073741824
- KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS=300000
- KAFKA_COMPRESSION_TYPE=gzip
ports:
- 29092:29092
- 9092:9092
- 9094:9094
volumes:
- kafka_data:/var/lib/kafka/data
volumes:
zookeeper_data:
kafka_data:
networks:
evolution-net:
name: evolution-net
driver: bridge

View File

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

View File

@@ -15,6 +15,16 @@ 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

@@ -20,6 +20,15 @@ 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 25 KiB

485
manager/dist/assets/index-CO3NSIFj.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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-DJ2Q5K8k.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DxAxQfZR.css">
<script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
</head>
<body>
<div id="root"></div>

8
manager_install.sh Executable file
View File

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

5920
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.4",
"version": "2.3.5",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js",
"type": "commonjs",
@@ -56,7 +56,7 @@
"eslint --fix"
],
"src/**/*.ts": [
"sh -c 'npm run build'"
"sh -c 'tsc --noEmit'"
]
},
"config": {
@@ -77,7 +77,7 @@
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "^7.0.0-rc.3",
"baileys": "^7.0.0-rc.5",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",

View File

@@ -1,5 +1,5 @@
-- CreateTable
CREATE TABLE "public"."Kafka" (
CREATE TABLE "Kafka" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"events" JSONB NOT NULL,
@@ -11,7 +11,7 @@ CREATE TABLE "public"."Kafka" (
);
-- CreateIndex
CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "public"."Kafka"("instanceId");
CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "Kafka"("instanceId");
-- AddForeignKey
ALTER TABLE "public"."Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -16,6 +16,7 @@ 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';
@@ -171,6 +172,8 @@ export class EvolutionStartupService extends ChannelStartupService {
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({

View File

@@ -24,6 +24,7 @@ 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';
@@ -655,6 +656,8 @@ export class BusinessStartupService extends ChannelStartupService {
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({

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, force);
const response = await baileys_sock.assertSessions(jids);
callback(response);

View File

@@ -85,6 +85,7 @@ import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
import { makeProxyAgent } from '@utils/makeProxyAgent';
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
import { status } from '@utils/renderStatus';
import { sendTelemetry } from '@utils/sendTelemetry';
import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma';
import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files';
import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db';
@@ -152,13 +153,7 @@ import { v4 } from 'uuid';
import { BaileysMessageProcessor } from './baileysMessage.processor';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
export interface ExtendedMessageKey extends WAMessageKey {
senderPn?: string;
previousRemoteJid?: string | null;
}
export interface ExtendedIMessageKey extends proto.IMessageKey {
senderPn?: string;
remoteJidAlt?: string;
participantAlt?: string;
server_id?: string;
@@ -254,6 +249,10 @@ export class BaileysStartupService extends ChannelStartupService {
private endSession = false;
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
// Cache TTL constants (in seconds)
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates
public stateConnection: wa.StateConnection = { state: 'close' };
public phoneNumber: string;
@@ -1000,10 +999,6 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (m.key.remoteJid?.includes('@lid') && (m.key as ExtendedIMessageKey).senderPn) {
m.key.remoteJid = (m.key as ExtendedIMessageKey).senderPn;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
@@ -1066,10 +1061,6 @@ export class BaileysStartupService extends ChannelStartupService {
) => {
try {
for (const received of messages) {
if (received.key.remoteJid?.includes('@lid') && (received.key as ExtendedMessageKey).senderPn) {
(received.key as ExtendedMessageKey).previousRemoteJid = received.key.remoteJid;
received.key.remoteJid = (received.key as ExtendedMessageKey).senderPn;
}
if (
received?.messageStubParameters?.some?.((param) =>
[
@@ -1117,9 +1108,9 @@ export class BaileysStartupService extends ChannelStartupService {
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const oldMessage = await this.getMessage(editedMessage.key, true);
if ((oldMessage as any)?.id) {
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
? Math.floor(editedMessage.timestampMs.toNumber() / 1000)
: Math.floor((editedMessage.timestampMs as number) / 1000);
const editedMessageTimestamp = Long.isLong(received?.messageTimestamp)
? Math.floor(received?.messageTimestamp.toNumber())
: Math.floor(received?.messageTimestamp as number);
await this.prismaRepository.message.update({
where: { id: (oldMessage as any).id },
@@ -1150,7 +1141,7 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
await this.baileysCache.set(messageKey, true, 5 * 60);
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
if (
(type !== 'notify' && type !== 'append') ||
@@ -1270,7 +1261,7 @@ export class BaileysStartupService extends ChannelStartupService {
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
}
await this.baileysCache.set(messageKey, true, 5 * 60);
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
} else {
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
}
@@ -1358,12 +1349,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
}
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({
@@ -1437,9 +1426,7 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (key.remoteJid?.includes('@lid') && key.remoteJidAlt) {
key.remoteJid = key.remoteJidAlt;
}
if (update.message !== null && update.status === undefined) continue;
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
@@ -1480,7 +1467,7 @@ export class BaileysStartupService extends ChannelStartupService {
keyId: key.id,
remoteJid: key?.remoteJid,
fromMe: key.fromMe,
participant: key?.remoteJid,
participant: key?.participant,
status: status[update.status] ?? 'DELETED',
pollUpdates,
instanceId: this.instanceId,
@@ -1498,7 +1485,11 @@ export class BaileysStartupService extends ChannelStartupService {
`) as any[];
findMessage = messages[0] || null;
if (findMessage) message.messageId = findMessage.id;
if (!findMessage?.id) {
this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`);
continue;
}
message.messageId = findMessage.id;
}
if (update.message === null && update.status === undefined) {
@@ -1533,7 +1524,7 @@ export class BaileysStartupService extends ChannelStartupService {
if (status[update.status] === status[4]) {
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
await this.baileysCache.set(messageKey, true, 5 * 60);
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
}
await this.prismaRepository.message.update({
@@ -1591,12 +1582,66 @@ export class BaileysStartupService extends ChannelStartupService {
});
},
'group-participants.update': (participantsUpdate: {
'group-participants.update': async (participantsUpdate: {
id: string;
participants: string[];
action: ParticipantAction;
}) => {
// ENHANCEMENT: Adds participantsData field while maintaining backward compatibility
// MAINTAINS: participants: string[] (original JID strings)
// ADDS: participantsData: { jid: string, phoneNumber: string, name?: string, imgUrl?: string }[]
// This enables LID to phoneNumber conversion without breaking existing webhook consumers
// Helper to normalize participantId as phone number
const normalizePhoneNumber = (id: string): string => {
// Remove @lid, @s.whatsapp.net suffixes and extract just the number part
return id.split('@')[0];
};
try {
// Usa o mesmo método que o endpoint /group/participants
const groupParticipants = await this.findParticipants({ groupJid: participantsUpdate.id });
// Validação para garantir que temos dados válidos
if (!groupParticipants?.participants || !Array.isArray(groupParticipants.participants)) {
throw new Error('Invalid participant data received from findParticipants');
}
// Filtra apenas os participantes que estão no evento
const resolvedParticipants = participantsUpdate.participants.map((participantId) => {
const participantData = groupParticipants.participants.find((p) => p.id === participantId);
let phoneNumber: string;
if (participantData?.phoneNumber) {
phoneNumber = participantData.phoneNumber;
} else {
phoneNumber = normalizePhoneNumber(participantId);
}
return {
jid: participantId,
phoneNumber,
name: participantData?.name,
imgUrl: participantData?.imgUrl,
};
});
// Mantém formato original + adiciona dados resolvidos
const enhancedParticipantsUpdate = {
...participantsUpdate,
participants: participantsUpdate.participants, // Mantém array original de strings
// Adiciona dados resolvidos em campo separado
participantsData: resolvedParticipants,
};
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, enhancedParticipantsUpdate);
} catch (error) {
this.logger.error(
`Failed to resolve participant data for GROUP_PARTICIPANTS_UPDATE webhook: ${error.message} | Group: ${participantsUpdate.id} | Participants: ${participantsUpdate.participants.length}`,
);
// Fallback - envia sem conversão
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate);
}
this.updateGroupMetadataCache(participantsUpdate.id);
},
@@ -1678,6 +1723,9 @@ export class BaileysStartupService extends ChannelStartupService {
}
if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') {
if (call.from.endsWith('@lid')) {
call.from = await this.client.signalRepository.lidMapping.getPNForLID(call.from as string);
}
const msg = await this.client.sendMessage(call.from, { text: settings.msgCall });
this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify' });
@@ -3367,18 +3415,13 @@ export class BaileysStartupService extends ChannelStartupService {
}
const numberJid = numberVerified?.jid || user.jid;
const lid =
typeof numberVerified?.lid === 'string'
? numberVerified.lid
: numberJid.includes('@lid')
? numberJid.split('@')[1]
: undefined;
return new OnWhatsAppDto(
numberJid,
!!numberVerified?.exists,
user.number,
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
lid,
undefined,
);
}),
);
@@ -3530,7 +3573,7 @@ export class BaileysStartupService extends ChannelStartupService {
keyId: messageId,
remoteJid: response.key.remoteJid,
fromMe: response.key.fromMe,
participant: response.key?.remoteJid,
participant: response.key?.participant,
status: 'DELETED',
instanceId: this.instanceId,
};
@@ -3590,7 +3633,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
if (
Object.keys(msg.message).length === 1 &&
Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo')
) {
throw 'The message is messageContextInfo';
}
@@ -3965,7 +4011,7 @@ export class BaileysStartupService extends ChannelStartupService {
keyId: messageId,
remoteJid: messageSent.key.remoteJid,
fromMe: messageSent.key.fromMe,
participant: messageSent.key?.remoteJid,
participant: messageSent.key?.participant,
status: 'EDITED',
instanceId: this.instanceId,
};
@@ -4561,8 +4607,8 @@ export class BaileysStartupService extends ChannelStartupService {
return response;
}
public async baileysAssertSessions(jids: string[], force: boolean) {
const response = await this.client.assertSessions(jids, force);
public async baileysAssertSessions(jids: string[]) {
const response = await this.client.assertSessions(jids);
return response;
}
@@ -4766,7 +4812,7 @@ export class BaileysStartupService extends ChannelStartupService {
{
OR: [
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
keyFilters?.senderPn ? { key: { path: ['senderPn'], equals: keyFilters?.senderPn } } : {},
keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {},
],
},
],
@@ -4796,7 +4842,7 @@ export class BaileysStartupService extends ChannelStartupService {
{
OR: [
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
keyFilters?.senderPn ? { key: { path: ['senderPn'], equals: keyFilters?.senderPn } } : {},
keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {},
],
},
],

View File

@@ -1,6 +1,5 @@
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';
@@ -24,7 +23,7 @@ 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 { proto } from 'baileys';
import { proto, WAMessageKey } from 'baileys';
import dayjs from 'dayjs';
import FormData from 'form-data';
import { Jimp, JimpMime } from 'jimp';
@@ -33,6 +32,8 @@ import mimeTypes from 'mime-types';
import path from 'path';
import { Readable } from 'stream';
const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds
interface ChatwootMessage {
messageId?: number;
inboxId?: number;
@@ -44,6 +45,25 @@ interface ChatwootMessage {
export class ChatwootService {
private readonly logger = new Logger('ChatwootService');
// HTTP timeout constants
private readonly MEDIA_DOWNLOAD_TIMEOUT_MS = 60000; // 60 seconds for large files
// S3/MinIO retry configuration (external storage - longer delays, fewer retries)
private readonly S3_MAX_RETRIES = 3;
private readonly S3_BASE_DELAY_MS = 1000; // Base delay: 1 second
private readonly S3_MAX_DELAY_MS = 8000; // Max delay: 8 seconds
// Database polling retry configuration (internal DB - shorter delays, more retries)
private readonly DB_POLLING_MAX_RETRIES = 5;
private readonly DB_POLLING_BASE_DELAY_MS = 100; // Base delay: 100ms
private readonly DB_POLLING_MAX_DELAY_MS = 2000; // Max delay: 2 seconds
// Webhook processing delay
private readonly WEBHOOK_INITIAL_DELAY_MS = 500; // Initial delay before processing webhook
// Lock polling delay
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
private provider: any;
constructor(
@@ -568,27 +588,29 @@ export class ChatwootService {
}
public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = body.key.remoteJid;
const isLid = body.key.addressingMode === 'lid' && body.key.remoteJidAlt;
const remoteJid = isLid ? body.key.remoteJidAlt : body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds
const maxWaitTime = 5000; // 5 seconds
const client = await this.clientCw(instance);
if (!client) return null;
try {
// Processa atualização de contatos já criados @lid
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
if (isLid && body.key.remoteJidAlt !== body.key.remoteJid) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
if (contact && contact.identifier !== body.key.remoteJidAlt) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.remoteJidAlt: ${body.key.remoteJidAlt}`,
);
const updateContact = await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`,
identifier: body.key.remoteJidAlt,
phone_number: `+${body.key.remoteJidAlt.split('@')[0]}`,
});
if (updateContact === null) {
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
const baseContact = await this.findContact(instance, body.key.remoteJidAlt.split('@')[0]);
if (baseContact) {
await this.mergeContacts(baseContact.id, contact.id);
this.logger.verbose(
@@ -605,6 +627,22 @@ export class ChatwootService {
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
let conversationExists: conversation | boolean;
try {
conversationExists = await client.conversations.get({
accountId: this.provider.accountId,
conversationId: conversationId,
});
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
} catch (error) {
this.logger.error(`Error getting conversation: ${error}`);
conversationExists = false;
}
if (!conversationExists) {
this.logger.verbose('Conversation does not exist, re-calling createConversation');
this.cache.delete(cacheKey);
return await this.createConversation(instance, body);
}
return conversationId;
}
@@ -617,7 +655,7 @@ export class ChatwootService {
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
break;
}
await new Promise((res) => setTimeout(res, 300));
await new Promise((res) => setTimeout(res, this.LOCK_POLLING_DELAY_MS));
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
@@ -639,11 +677,8 @@ export class ChatwootService {
return (await this.cache.get(cacheKey)) as number;
}
const client = await this.clientCw(instance);
if (!client) return null;
const isGroup = remoteJid.includes('@g.us');
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0].split(':')[0];
let nameContact = !body.key.fromMe ? body.pushName : chatId;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
@@ -769,7 +804,7 @@ export class ChatwootService {
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
this.cache.set(cacheKey, inboxConversation.id, 8 * 3600);
return inboxConversation.id;
}
}
@@ -802,7 +837,7 @@ export class ChatwootService {
}
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
this.cache.set(cacheKey, conversation.id, 8 * 3600);
return conversation.id;
} finally {
await this.cache.delete(lockKey);
@@ -1123,20 +1158,140 @@ export class ChatwootService {
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
try {
const parsedMedia = path.parse(decodeURIComponent(media));
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
let fileName = parsedMedia?.name + parsedMedia?.ext;
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
// URLs presigned podem expirar, então convertemos para base64
let mediaBuffer: Buffer;
let mimeType: string;
let fileName: string;
if (!mimeType) {
const parts = media.split('/');
fileName = decodeURIComponent(parts[parts.length - 1]);
try {
this.logger.verbose(`Downloading media from: ${media}`);
// Tentar fazer download do arquivo com autenticação do Chatwoot
// maxRedirects: 0 para não seguir redirects automaticamente
const response = await axios.get(media, {
responseType: 'arraybuffer',
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
headers: {
api_access_token: this.provider.token,
},
maxRedirects: 0, // Não seguir redirects automaticamente
validateStatus: (status) => status < 500, // Aceitar redirects (301, 302, 307)
});
mimeType = response.headers['content-type'];
this.logger.verbose(`Initial response status: ${response.status}`);
// Se for redirect, pegar a URL de destino e fazer novo request
if (response.status >= 300 && response.status < 400) {
const redirectUrl = response.headers.location;
this.logger.verbose(`Redirect to: ${redirectUrl}`);
if (redirectUrl) {
// Fazer novo request para a URL do S3/MinIO (sem autenticação, pois é presigned URL)
// IMPORTANTE: Chatwoot pode gerar a URL presigned ANTES de fazer upload
// Vamos tentar com retry usando exponential backoff se receber 404 (arquivo ainda não disponível)
this.logger.verbose('Downloading from S3/MinIO...');
let s3Response;
let retryCount = 0;
const maxRetries = this.S3_MAX_RETRIES;
const baseDelay = this.S3_BASE_DELAY_MS;
const maxDelay = this.S3_MAX_DELAY_MS;
while (retryCount <= maxRetries) {
s3Response = await axios.get(redirectUrl, {
responseType: 'arraybuffer',
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
validateStatus: (status) => status < 500,
});
this.logger.verbose(
`S3 response status: ${s3Response.status}, size: ${s3Response.data?.byteLength || 0} bytes (attempt ${retryCount + 1}/${maxRetries + 1})`,
);
// Se não for 404, sair do loop
if (s3Response.status !== 404) {
break;
}
// Se for 404 e ainda tem tentativas, aguardar com exponential backoff e tentar novamente
if (retryCount < maxRetries) {
// Exponential backoff com max delay (seguindo padrão do webhook controller)
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
this.logger.warn(
`File not yet available in S3/MinIO (attempt ${retryCount + 1}/${maxRetries + 1}). Retrying in ${backoffDelay}ms with exponential backoff...`,
);
this.logger.verbose(`MinIO Response: ${errorBody}`);
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
retryCount++;
} else {
// Última tentativa falhou
break;
}
}
// Após todas as tentativas, verificar o status final
if (s3Response.status === 404) {
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
this.logger.error(`File not found in S3/MinIO after ${maxRetries + 1} attempts. URL: ${redirectUrl}`);
this.logger.error(`MinIO Error Response: ${errorBody}`);
throw new Error(
'File not found in S3/MinIO (404). The file may have been deleted, the URL is incorrect, or Chatwoot has not finished uploading yet.',
);
}
if (s3Response.status === 403) {
this.logger.error(`Access denied to S3/MinIO. URL may have expired: ${redirectUrl}`);
throw new Error(
'Access denied to S3/MinIO (403). Presigned URL may have expired. Check S3_PRESIGNED_EXPIRATION setting.',
);
}
if (s3Response.status >= 400) {
this.logger.error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
throw new Error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
}
mediaBuffer = Buffer.from(s3Response.data);
mimeType = s3Response.headers['content-type'] || 'application/octet-stream';
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes from S3, type: ${mimeType}`);
} else {
this.logger.error('Redirect response without Location header');
throw new Error('Redirect without Location header');
}
} else if (response.status === 404) {
this.logger.error(`File not found (404) at: ${media}`);
throw new Error('File not found (404). The attachment may not exist in Chatwoot storage.');
} else if (response.status >= 400) {
this.logger.error(`HTTP ${response.status}: ${response.statusText} for URL: ${media}`);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} else {
// Download direto sem redirect
mediaBuffer = Buffer.from(response.data);
mimeType = response.headers['content-type'] || 'application/octet-stream';
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes directly, type: ${mimeType}`);
}
// Extrair nome do arquivo da URL ou usar o content-disposition
const parsedMedia = path.parse(decodeURIComponent(media));
if (parsedMedia?.name && parsedMedia?.ext) {
fileName = parsedMedia.name + parsedMedia.ext;
} else {
const parts = media.split('/');
fileName = decodeURIComponent(parts[parts.length - 1].split('?')[0]);
}
this.logger.verbose(`File name: ${fileName}, size: ${mediaBuffer.length} bytes`);
} catch (downloadError) {
this.logger.error('[MEDIA DOWNLOAD] ❌ Error downloading media from: ' + media);
this.logger.error(`[MEDIA DOWNLOAD] Error message: ${downloadError.message}`);
this.logger.error(`[MEDIA DOWNLOAD] Error stack: ${downloadError.stack}`);
this.logger.error(`[MEDIA DOWNLOAD] Full error: ${JSON.stringify(downloadError, null, 2)}`);
throw new Error(`Failed to download media: ${downloadError.message}`);
}
// Determinar o tipo de mídia pelo mimetype
let type = 'document';
switch (mimeType.split('/')[0]) {
@@ -1154,10 +1309,12 @@ export class ChatwootService {
break;
}
// Para áudio, usar base64 com data URI
if (type === 'audio') {
const base64Audio = `data:${mimeType};base64,${mediaBuffer.toString('base64')}`;
const data: SendAudioDto = {
number: number,
audio: media,
audio: base64Audio,
delay: 1200,
quoted: options?.quoted,
};
@@ -1169,8 +1326,12 @@ export class ChatwootService {
return messageSent;
}
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
const base64Media = mediaBuffer.toString('base64');
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
const parsedExt = path.parse(fileName)?.ext;
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
type = 'document';
}
@@ -1178,7 +1339,7 @@ export class ChatwootService {
number: number,
mediatype: type as any,
fileName: fileName,
media: media,
media: base64Media, // Base64 puro, sem prefixo
delay: 1200,
quoted: options?.quoted,
};
@@ -1194,6 +1355,7 @@ export class ChatwootService {
return messageSent;
} catch (error) {
this.logger.error(error);
throw error; // Re-throw para que o erro seja tratado pelo caller
}
}
@@ -1233,9 +1395,87 @@ export class ChatwootService {
});
}
/**
* Processa deleção de mensagem em background
* Método assíncrono chamado via setImmediate para não bloquear resposta do webhook
*/
private async processDeletion(instance: InstanceDto, body: any, deleteLockKey: string) {
this.logger.warn(`[DELETE] 🗑️ Processing deletion - messageId: ${body.id}`);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
// Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos)
const messages = await this.prismaRepository.message.findMany({
where: {
chatwootMessageId: body.id,
instanceId: instance.instanceId,
},
});
if (messages && messages.length > 0) {
this.logger.warn(`[DELETE] Found ${messages.length} message(s) to delete from Chatwoot message ${body.id}`);
this.logger.verbose(`[DELETE] Messages keys: ${messages.map((m) => (m.key as any)?.id).join(', ')}`);
// Deletar cada mensagem no WhatsApp
for (const message of messages) {
const key = message.key as WAMessageKey;
this.logger.warn(
`[DELETE] Attempting to delete WhatsApp message - keyId: ${key?.id}, remoteJid: ${key?.remoteJid}`,
);
try {
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
this.logger.warn(`[DELETE] ✅ Message ${key.id} deleted in WhatsApp successfully`);
} catch (error) {
this.logger.error(`[DELETE] ❌ Error deleting message ${key.id} in WhatsApp: ${error}`);
this.logger.error(`[DELETE] Error details: ${JSON.stringify(error, null, 2)}`);
}
}
// Remover todas as mensagens do banco de dados
await this.prismaRepository.message.deleteMany({
where: {
instanceId: instance.instanceId,
chatwootMessageId: body.id,
},
});
this.logger.warn(`[DELETE] ✅ SUCCESS: ${messages.length} message(s) deleted from WhatsApp and database`);
} else {
// Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
this.logger.warn(`[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${body.id}`);
}
// Liberar lock após processar
await this.cache.delete(deleteLockKey);
}
public async receiveWebhook(instance: InstanceDto, body: any) {
try {
await new Promise((resolve) => setTimeout(resolve, 500));
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
// para evitar race condition com webhooks duplicados
let isDeletionEvent = false;
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
isDeletionEvent = true;
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
// Verificar se já está processando esta deleção
if (await this.cache.has(deleteLockKey)) {
this.logger.warn(`[DELETE] ⏭️ SKIPPING: Deletion already in progress for messageId: ${body.id}`);
return { message: 'already_processing' };
}
// Adquirir lock IMEDIATAMENTE por 30 segundos
await this.cache.set(deleteLockKey, true, 30);
this.logger.warn(
`[WEBHOOK-DELETE] Event: ${body.event}, messageId: ${body.id}, conversation: ${body.conversation?.id}`,
);
}
// Para deleções, processar IMEDIATAMENTE (sem delay)
// Para outros eventos, aguardar delay inicial
if (!isDeletionEvent) {
await new Promise((resolve) => setTimeout(resolve, this.WEBHOOK_INITIAL_DELAY_MS));
}
const client = await this.clientCw(instance);
@@ -1254,6 +1494,39 @@ export class ChatwootService {
this.cache.delete(keyToDelete);
}
// Log para debug de mensagens deletadas
if (body.event === 'message_updated') {
this.logger.verbose(
`Message updated event - deleted: ${body.content_attributes?.deleted}, messageId: ${body.id}`,
);
}
// Processar deleção de mensagem ANTES das outras validações
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
// Lock já foi adquirido no início do método (antes do delay)
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
// ESTRATÉGIA: Processar em background e responder IMEDIATAMENTE
// Isso evita timeout do Chatwoot (5s) quando há muitas imagens (> 5s de processamento)
this.logger.warn(`[DELETE] 🚀 Starting background deletion - messageId: ${body.id}`);
// Executar em background (sem await) - não bloqueia resposta do webhook
setImmediate(async () => {
try {
await this.processDeletion(instance, body, deleteLockKey);
} catch (error) {
this.logger.error(`[DELETE] ❌ Background deletion failed for messageId ${body.id}: ${error}`);
}
});
// RESPONDER IMEDIATAMENTE ao Chatwoot (< 50ms)
return {
message: 'deletion_accepted',
messageId: body.id,
note: 'Deletion is being processed in background',
};
}
if (
!body?.conversation ||
body.private ||
@@ -1285,7 +1558,7 @@ export class ChatwootService {
});
if (message) {
const key = message.key as ExtendedMessageKey;
const key = message.key as WAMessageKey;
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
@@ -1370,7 +1643,10 @@ export class ChatwootService {
}
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
if (
body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' &&
body?.conversation?.messages[0]?.id === body?.id
) {
return { message: 'bot' };
}
@@ -1394,6 +1670,8 @@ export class ChatwootService {
for (const message of body.conversation.messages) {
if (message.attachments && message.attachments.length > 0) {
// Processa anexos de forma assíncrona para não bloquear o webhook
const processAttachments = async () => {
for (const attachment of message.attachments) {
if (!messageReceived) {
formatText = null;
@@ -1403,6 +1681,7 @@ export class ChatwootService {
quoted: await this.getQuotedMessage(body, instance),
};
try {
const messageSent = await this.sendAttachment(
waInstance,
chatId,
@@ -1410,10 +1689,12 @@ export class ChatwootService {
formatText,
options,
);
if (!messageSent && body.conversation?.id) {
this.onSendMessageError(instance, body.conversation?.id);
}
if (messageSent) {
await this.updateChatwootMessageId(
{
...messageSent,
@@ -1428,6 +1709,19 @@ export class ChatwootService {
instance,
);
}
} catch (error) {
this.logger.error(error);
if (body.conversation?.id) {
this.onSendMessageError(instance, body.conversation?.id, error);
}
}
}
};
// Executa em background sem bloquear
processAttachments().catch((error) => {
this.logger.error(error);
});
} else {
const data: SendTextDto = {
number: chatId,
@@ -1450,10 +1744,7 @@ export class ChatwootService {
}
await this.updateChatwootMessageId(
{
...messageSent,
instanceId: instance.instanceId,
},
messageSent, // Já tem instanceId
{
messageId: body.id,
inboxId: body.inbox?.id,
@@ -1483,7 +1774,7 @@ export class ChatwootService {
},
});
if (lastMessage && !lastMessage.chatwootIsRead) {
const key = lastMessage.key as ExtendedMessageKey;
const key = lastMessage.key as WAMessageKey;
waInstance?.markMessageAsRead({
readMessages: [
@@ -1541,14 +1832,63 @@ export class ChatwootService {
chatwootMessageIds: ChatwootMessage,
instance: InstanceDto,
) {
const key = message.key as ExtendedMessageKey;
const key = message.key as WAMessageKey;
if (!chatwootMessageIds.messageId || !key?.id) {
this.logger.verbose(
`Skipping updateChatwootMessageId - messageId: ${chatwootMessageIds.messageId}, keyId: ${key?.id}`,
);
return;
}
// Use instanceId from message or fallback to instance
const instanceId = message.instanceId || instance.instanceId;
this.logger.verbose(
`Updating message with chatwootMessageId: ${chatwootMessageIds.messageId}, keyId: ${key.id}, instanceId: ${instanceId}`,
);
// Verifica se a mensagem existe antes de atualizar usando polling com exponential backoff
let retries = 0;
const maxRetries = this.DB_POLLING_MAX_RETRIES;
const baseDelay = this.DB_POLLING_BASE_DELAY_MS;
const maxDelay = this.DB_POLLING_MAX_DELAY_MS;
let messageExists = false;
while (retries < maxRetries && !messageExists) {
const existingMessage = await this.prismaRepository.message.findFirst({
where: {
instanceId: instanceId,
key: {
path: ['id'],
equals: key.id,
},
},
});
if (existingMessage) {
messageExists = true;
this.logger.verbose(`Message found in database after ${retries} retries`);
} else {
retries++;
if (retries < maxRetries) {
// Exponential backoff com max delay (seguindo padrão do sistema)
const backoffDelay = Math.min(baseDelay * Math.pow(2, retries - 1), maxDelay);
this.logger.verbose(`Message not found, retry ${retries}/${maxRetries} in ${backoffDelay}ms`);
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
} else {
this.logger.verbose(`Message not found after ${retries} attempts`);
}
}
}
if (!messageExists) {
this.logger.warn(`Message not found in database after ${maxRetries} retries, keyId: ${key.id}`);
return;
}
// Use raw SQL to avoid JSON path issues
await this.prismaRepository.$executeRaw`
const result = await this.prismaRepository.$executeRaw`
UPDATE "Message"
SET
"chatwootMessageId" = ${chatwootMessageIds.messageId},
@@ -1556,10 +1896,12 @@ export class ChatwootService {
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
WHERE "instanceId" = ${instance.instanceId}
WHERE "instanceId" = ${instanceId}
AND "key"->>'id' = ${key.id}
`;
this.logger.verbose(`Update result: ${result} rows affected`);
if (this.isImportHistoryAvailable()) {
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
}
@@ -1609,7 +1951,7 @@ export class ChatwootService {
},
});
const key = message?.key as ExtendedMessageKey;
const key = message?.key as WAMessageKey;
if (message && key?.id) {
return {
@@ -1913,6 +2255,7 @@ 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;
}
@@ -2235,9 +2578,8 @@ export class ChatwootService {
}
if (event === 'messages.edit' || event === 'send.message.update') {
const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
const editedMessageContent =
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
const message = await this.getMessageByKeyId(instance, body?.key?.id);
if (!message) {
@@ -2245,11 +2587,14 @@ export class ChatwootService {
return;
}
const key = message.key as ExtendedMessageKey;
const key = message.key as WAMessageKey;
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
if (message && message.chatwootConversationId) {
if (message && message.chatwootConversationId && message.chatwootMessageId) {
// Criar nova mensagem com formato: "Mensagem editada:\n\nteste1"
const editedText = `\n\n\`${i18next.t('cw.message.edited')}:\`\n\n${editedMessageContent}`;
const send = await this.createMessage(
instance,
message.chatwootConversationId,
@@ -2327,15 +2672,30 @@ export class ChatwootService {
await this.createBotMessage(instance, msgStatus, 'incoming');
}
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) {
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');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
waInstance.qrCode.count = 0;
waInstance.lastConnectionNotification = now;
chatwootImport.clearAll(instance);
}
// Se não foi via QR code, verifica o throttling.
else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) {
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)`,
);
}
}

View File

@@ -112,12 +112,19 @@ class ChatwootImport {
const bindInsert = [provider.accountId];
for (const contact of contactsChunk) {
bindInsert.push(contact.pushName);
const isGroup = this.isIgnorePhoneNumber(contact.remoteJid);
const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName;
bindInsert.push(contactName);
const bindName = `$${bindInsert.length}`;
let bindPhoneNumber: string;
if (!isGroup) {
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
const bindPhoneNumber = `$${bindInsert.length}`;
bindPhoneNumber = `$${bindInsert.length}`;
} else {
bindPhoneNumber = 'NULL';
}
bindInsert.push(contact.remoteJid);
const bindIdentifier = `$${bindInsert.length}`;

View File

@@ -826,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 && 'messageContextInfo' in msg) {
if (Object.keys(msg).length === 1 && Object.prototype.hasOwnProperty.call(msg, 'messageContextInfo')) {
return false;
}

View File

@@ -3,8 +3,6 @@ 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();