Compare commits

...

78 Commits
2.3.4 ... 2.3.6

Author SHA1 Message Date
Davidson Gomes
3454bec79f Merge branch 'release/2.3.6'
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-21 11:40:39 -03:00
Davidson Gomes
8c27f11f5b chore(changelog): update to version 2.3.6
Some checks failed
Security Scan / Dependency Review (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (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
2025-10-21 11:40:27 -03:00
Davidson Gomes
ae9f3efeff Merge pull request #2108 from KokeroO/develop
chore: bump version to 2.3.6 and update baileys dependency to 7.0.0-rc.6
2025-10-21 09:00:58 -03:00
Willian Coqueiro
ba3a2fae59 chore: atualizar versão da API para 2.3.6 nos templates e configurações do Docker 2025-10-20 03:06:37 +00:00
Willian Coqueiro
aa0d793d26 chore: bump version to 2.3.6 and update baileys dependency to 7.0.0-rc.6 2025-10-20 02:24:56 +00:00
Davidson Gomes
48bda1b5af Merge pull request #2107 from KokeroO/develop
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( baileys.service ): Corrige ao salvar no DB valores Uint8Array
2025-10-19 17:59:26 -03:00
Willian Coqueiro
dd21a29ea6 fix(baileys): salvar corretamente buffer no db 2025-10-19 18:53:16 +00:00
Davidson Gomes
e83a7e2e88 Merge pull request #2105 from KokeroO/develop
fix: Simplify logging of messageSent object
2025-10-19 06:41:28 -03:00
Willian Coqueiro
d58d0b8bff Merge branch 'EvolutionAPI:develop' into develop 2025-10-19 06:34:58 -03:00
Willian Coqueiro
4efc9b65bc Simplify logging of messageSent object
Evita o erro de this.isZero not is function
2025-10-19 06:34:45 -03:00
Davidson Gomes
cd71ff503d Merge pull request #2103 from KokeroO/develop
feat(baileys,chatwoot,on-whatsapp-cache): implementações e correções na baileys e chatwoot
2025-10-19 06:23:39 -03:00
Willian Coqueiro
582166e5ae fix(lint): lint 2025-10-19 05:41:55 +00:00
Willian Coqueiro
e1ae03c1e4 fix(comments): comments fix 2025-10-19 05:37:22 +00:00
Willian Coqueiro
0737c45df2 Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-10-19 02:29:04 -03:00
Willian Coqueiro
adbe1079d5 Update src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-10-19 02:28:45 -03:00
Willian Coqueiro
423f629b04 Update src/utils/onWhatsappCache.ts
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-10-19 02:26:52 -03:00
Willian Coqueiro
946dcaeb2e feat(baileys,chatwoot,on-whatsapp-cache): implementações e correções na baileys e chatwoot
* corrige cache de números PN, LIDs e g.us para enviar o número correto
* atualiza para os últimos commits da baileys
* corrige envio de áudio e documentos via chatwoot no canal baileys
* diversas correções na integração com chatwoot
* corrige mensagens ignoradas no recebimento de leads
2025-10-19 03:05:11 +00:00
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
32 changed files with 3489 additions and 2722 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.6]
- 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,72 @@
# 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

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.6
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

4330
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.6",
"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.6",
"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

@@ -1,5 +1,5 @@
import { Logger } from '@config/logger.config';
import { BaileysEventMap, MessageUpsertType, proto } from 'baileys';
import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys';
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
@@ -12,7 +12,7 @@ export class BaileysMessageProcessor {
private subscription?: Subscription;
protected messageSubject = new Subject<{
messages: proto.IWebMessageInfo[];
messages: WAMessage[];
type: MessageUpsertType;
requestId?: string;
settings: any;

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';
@@ -132,7 +133,6 @@ import { Label } from 'baileys/lib/Types/Label';
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
import { spawn } from 'child_process';
import { isArray, isBase64, isURL } from 'class-validator';
import { randomBytes } from 'crypto';
import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg';
import FormData from 'form-data';
@@ -152,13 +152,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 +248,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;
@@ -500,8 +498,8 @@ export class BaileysStartupService extends ChannelStartupService {
try {
// Use raw SQL to avoid JSON path issues
const webMessageInfo = (await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'id' = ${key.id}
`) as proto.IWebMessageInfo[];
@@ -877,6 +875,7 @@ export class BaileysStartupService extends ChannelStartupService {
'contacts.update': async (contacts: Partial<Contact>[]) => {
const contactsRaw: { remoteJid: string; pushName?: string; profilePicUrl?: string; instanceId: string }[] = [];
for await (const contact of contacts) {
this.logger.debug(`Updating contact: ${JSON.stringify(contact, null, 2)}`);
contactsRaw.push({
remoteJid: contact.id,
pushName: contact?.name ?? contact?.verifiedName,
@@ -896,10 +895,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
await this.prismaRepository.$transaction(updateTransactions);
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
if (usersContacts) {
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
}
//const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
},
};
@@ -1000,10 +996,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 +1058,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 +1105,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 },
@@ -1145,12 +1133,12 @@ export class BaileysStartupService extends ChannelStartupService {
const messageKey = `${this.instance.id}_${received.key.id}`;
const cached = await this.baileysCache.get(messageKey);
if (cached && !editedMessage) {
if (cached && !editedMessage && !requestId) {
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
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 +1258,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,11 +1346,9 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
}
this.logger.verbose(messageRaw);
this.logger.log(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
@@ -1377,7 +1363,12 @@ export class BaileysStartupService extends ChannelStartupService {
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
});
const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = {
const contactRaw: {
remoteJid: string;
pushName: string;
profilePicUrl?: string;
instanceId: string;
} = {
remoteJid: received.key.remoteJid,
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
@@ -1388,6 +1379,17 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (contactRaw.remoteJid.includes('@s.whatsapp') || contactRaw.remoteJid.includes('@lid')) {
await saveOnWhatsappCache([
{
remoteJid:
messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid,
remoteJidAlt: messageRaw.key.remoteJidAlt,
lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null,
},
]);
}
if (contact) {
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
@@ -1417,10 +1419,6 @@ export class BaileysStartupService extends ChannelStartupService {
update: contactRaw,
create: contactRaw,
});
if (contactRaw.remoteJid.includes('@s.whatsapp')) {
await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]);
}
}
} catch (error) {
this.logger.error(error);
@@ -1428,7 +1426,7 @@ export class BaileysStartupService extends ChannelStartupService {
},
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
this.logger.verbose(`Update messages ${JSON.stringify(args, undefined, 2)}`);
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
@@ -1437,9 +1435,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 +1476,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,
@@ -1491,14 +1487,18 @@ export class BaileysStartupService extends ChannelStartupService {
if (configDatabaseData.HISTORIC || configDatabaseData.NEW_MESSAGE) {
// Use raw SQL to avoid JSON path issues
const messages = (await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'id' = ${key.id}
LIMIT 1
`) 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 +1533,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 +1591,66 @@ export class BaileysStartupService extends ChannelStartupService {
});
},
'group-participants.update': (participantsUpdate: {
'group-participants.update': async (participantsUpdate: {
id: string;
participants: string[];
action: ParticipantAction;
}) => {
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate);
// 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 +1732,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' });
@@ -1750,7 +1807,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
if (events['group-participants.update']) {
const payload = events['group-participants.update'];
const payload = events['group-participants.update'] as any;
this.groupHandler['group-participants.update'](payload);
}
}
@@ -1918,6 +1975,7 @@ export class BaileysStartupService extends ChannelStartupService {
quoted: any,
messageId?: string,
ephemeralExpiration?: number,
contextInfo?: any,
// participants?: GroupParticipant[],
) {
sender = sender.toLowerCase();
@@ -1934,8 +1992,8 @@ export class BaileysStartupService extends ChannelStartupService {
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
// NOTE: NÃO DEVEMOS GERAR O messageId AQUI, SOMENTE SE VIER INFORMADO POR PARAMETRO. A GERAÇÃO ANTERIOR IMPEDE O WZAP DE IDENTIFICAR A SOURCE.
if (messageId) option.messageId = messageId;
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
if (message['viewOnceMessage']) {
const m = generateWAMessageFromContent(sender, message, {
@@ -1972,10 +2030,19 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (contextInfo) {
message['contextInfo'] = contextInfo;
}
if (message['conversation']) {
return await this.client.sendMessage(
sender,
{ text: message['conversation'], mentions, linkPreview: linkPreview } as unknown as AnyMessageContent,
{
text: message['conversation'],
mentions,
linkPreview: linkPreview,
contextInfo: message['contextInfo'],
} as unknown as AnyMessageContent,
option as unknown as MiscMessageGenerationOptions,
);
}
@@ -1983,7 +2050,11 @@ export class BaileysStartupService extends ChannelStartupService {
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
return await this.client.sendMessage(
sender,
{ forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message }, mentions },
{
forward: { key: { remoteJid: this.instance.wuid, fromMe: true }, message },
mentions,
contextInfo: message['contextInfo'],
},
option as unknown as MiscMessageGenerationOptions,
);
}
@@ -2114,7 +2185,7 @@ export class BaileysStartupService extends ChannelStartupService {
if (options?.quoted) {
const m = options?.quoted;
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo);
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as WAMessage);
if (msg) {
quoted = msg;
@@ -2124,6 +2195,8 @@ export class BaileysStartupService extends ChannelStartupService {
let messageSent: WAMessage;
let mentions: string[];
let contextInfo: any;
if (isJidGroup(sender)) {
let group;
try {
@@ -2162,7 +2235,27 @@ export class BaileysStartupService extends ChannelStartupService {
// group?.participants,
);
} else {
messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted);
contextInfo = {
mentionedJid: [],
groupMentions: [],
//expiration: 7776000,
ephemeralSettingTimestamp: {
low: Math.floor(Date.now() / 1000) - 172800,
high: 0,
unsigned: false,
},
disappearingMode: { initiator: 0 },
};
messageSent = await this.sendMessage(
sender,
message,
mentions,
linkPreview,
quoted,
null,
undefined,
contextInfo,
);
}
if (Long.isLong(messageSent?.messageTimestamp)) {
@@ -2282,7 +2375,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
this.logger.log(messageRaw);
this.logger.verbose(messageSent);
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
@@ -3289,125 +3382,128 @@ export class BaileysStartupService extends ChannelStartupService {
where: { instanceId: this.instanceId, remoteJid: { in: jids.users.map(({ jid }) => jid) } },
});
// Separate @lid numbers from normal numbers
const lidUsers = jids.users.filter(({ jid }) => jid.includes('@lid'));
const normalUsers = jids.users.filter(({ jid }) => !jid.includes('@lid'));
// Unified cache verification for all numbers (normal and LID)
const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', ''));
// For normal numbers, use traditional Baileys verification
let normalVerifiedUsers: OnWhatsAppDto[] = [];
if (normalUsers.length > 0) {
console.log('normalUsers', normalUsers);
const numbersToVerify = normalUsers.map(({ jid }) => jid.replace('+', ''));
console.log('numbersToVerify', numbersToVerify);
// Get all numbers from cache
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
console.log('cachedNumbers', cachedNumbers);
// Separate numbers that are and are not in cache
const cachedJids = new Set(cachedNumbers.flatMap((cached) => cached.jidOptions));
const numbersNotInCache = numbersToVerify.filter((jid) => !cachedJids.has(jid));
const filteredNumbers = numbersToVerify.filter(
(jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)),
);
console.log('filteredNumbers', filteredNumbers);
// Only call Baileys for normal numbers (@s.whatsapp.net) that are not in cache
let verify: { jid: string; exists: boolean }[] = [];
const normalNumbersNotInCache = numbersNotInCache.filter((jid) => !jid.includes('@lid'));
const verify = await this.client.onWhatsApp(...filteredNumbers);
console.log('verify', verify);
normalVerifiedUsers = await Promise.all(
normalUsers.map(async (user) => {
let numberVerified: (typeof verify)[0] | null = null;
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
if (cached) {
return new OnWhatsAppDto(
cached.remoteJid,
true,
user.number,
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
cached.lid || (cached.remoteJid.includes('@lid') ? cached.remoteJid.split('@')[1] : undefined),
);
}
// Brazilian numbers
if (user.number.startsWith('55')) {
const numberWithDigit =
user.number.slice(4, 5) === '9' && user.number.length === 13
? user.number
: `${user.number.slice(0, 4)}9${user.number.slice(4)}`;
const numberWithoutDigit =
user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5);
numberVerified = verify.find(
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
);
}
// Mexican/Argentina numbers
// Ref: https://faq.whatsapp.com/1294841057948784
if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) {
let prefix = '';
if (user.number.startsWith('52')) {
prefix = '1';
}
if (user.number.startsWith('54')) {
prefix = '9';
}
const numberWithDigit =
user.number.slice(2, 3) === prefix && user.number.length === 13
? user.number
: `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`;
const numberWithoutDigit =
user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3);
numberVerified = verify.find(
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
);
}
if (!numberVerified) {
numberVerified = verify.find((v) => v.jid === user.jid);
}
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,
);
}),
);
if (normalNumbersNotInCache.length > 0) {
this.logger.verbose(`Checking ${normalNumbersNotInCache.length} numbers via Baileys (not found in cache)`);
verify = await this.client.onWhatsApp(...normalNumbersNotInCache);
}
// For @lid numbers, always consider them as valid
const lidVerifiedUsers: OnWhatsAppDto[] = lidUsers.map((user) => {
return new OnWhatsAppDto(
user.jid,
true,
user.number,
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
user.jid.split('@')[1],
);
});
const verifiedUsers = await Promise.all(
jids.users.map(async (user) => {
// Try to get from cache first (works for all: normal and LID)
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
if (cached) {
this.logger.verbose(`Number ${user.number} found in cache`);
return new OnWhatsAppDto(
cached.remoteJid,
true,
user.number,
contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
cached.lid || (cached.remoteJid.includes('@lid') ? 'lid' : undefined),
);
}
// If it's a LID number and not in cache, consider it valid
if (user.jid.includes('@lid')) {
return new OnWhatsAppDto(
user.jid,
true,
user.number,
contacts.find((c) => c.remoteJid === user.jid)?.pushName,
'lid',
);
}
// If not in cache and is a normal number, use Baileys verification
let numberVerified: (typeof verify)[0] | null = null;
// Brazilian numbers
if (user.number.startsWith('55')) {
const numberWithDigit =
user.number.slice(4, 5) === '9' && user.number.length === 13
? user.number
: `${user.number.slice(0, 4)}9${user.number.slice(4)}`;
const numberWithoutDigit =
user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5);
numberVerified = verify.find(
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
);
}
// Mexican/Argentina numbers
// Ref: https://faq.whatsapp.com/1294841057948784
if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) {
let prefix = '';
if (user.number.startsWith('52')) {
prefix = '1';
}
if (user.number.startsWith('54')) {
prefix = '9';
}
const numberWithDigit =
user.number.slice(2, 3) === prefix && user.number.length === 13
? user.number
: `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`;
const numberWithoutDigit =
user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3);
numberVerified = verify.find(
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
);
}
if (!numberVerified) {
numberVerified = verify.find((v) => v.jid === user.jid);
}
const numberJid = numberVerified?.jid || user.jid;
return new OnWhatsAppDto(
numberJid,
!!numberVerified?.exists,
user.number,
contacts.find((c) => c.remoteJid === numberJid)?.pushName,
undefined,
);
}),
);
// Combine results
onWhatsapp.push(...normalVerifiedUsers, ...lidVerifiedUsers);
onWhatsapp.push(...verifiedUsers);
// Save to cache only valid numbers
await saveOnWhatsappCache(
onWhatsapp
.filter((user) => user.exists)
.map((user) => ({
// TODO: Salvar no cache apenas números que NÃO estavam no cache
const numbersToCache = onWhatsapp.filter((user) => {
if (!user.exists) return false;
// Verifica se estava no cache usando jidOptions
const cached = cachedNumbers?.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
return !cached;
});
if (numbersToCache.length > 0) {
this.logger.verbose(`Salvando ${numbersToCache.length} números no cache`);
await saveOnWhatsappCache(
numbersToCache.map((user) => ({
remoteJid: user.jid,
jidOptions: user.jid.replace('+', ''),
lid: user.lid,
lid: user.lid === 'lid' ? 'lid' : undefined,
})),
);
);
}
return onWhatsapp;
}
@@ -3530,7 +3626,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,
};
@@ -3965,7 +4061,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,
};
@@ -4348,24 +4444,37 @@ export class BaileysStartupService extends ChannelStartupService {
throw new Error('Method not available in the Baileys service');
}
private convertLongToNumber(obj: any): any {
private deserializeMessageBuffers(obj: any): any {
if (obj === null || obj === undefined) {
return obj;
}
if (Long.isLong(obj)) {
return obj.toNumber();
if (typeof obj === 'object' && !Array.isArray(obj) && !Buffer.isBuffer(obj)) {
const keys = Object.keys(obj);
const isIndexedObject = keys.every((key) => !isNaN(Number(key)));
if (isIndexedObject && keys.length > 0) {
const values = keys.sort((a, b) => Number(a) - Number(b)).map((key) => obj[key]);
return new Uint8Array(values);
}
}
// Is Buffer?, converter to Uint8Array
if (Buffer.isBuffer(obj)) {
return new Uint8Array(obj);
}
// Process arrays recursively
if (Array.isArray(obj)) {
return obj.map((item) => this.convertLongToNumber(item));
return obj.map((item) => this.deserializeMessageBuffers(item));
}
// Process objects recursively
if (typeof obj === 'object') {
const converted: any = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
converted[key] = this.convertLongToNumber(obj[key]);
converted[key] = this.deserializeMessageBuffers(obj[key]);
}
}
return converted;
@@ -4386,8 +4495,8 @@ export class BaileysStartupService extends ChannelStartupService {
? 'Você'
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
status: status[message.status],
message: this.convertLongToNumber({ ...message.message }),
contextInfo: this.convertLongToNumber(contentMsg?.contextInfo),
message: this.deserializeMessageBuffers({ ...message.message }),
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),
messageType: contentType || 'unknown',
messageTimestamp: Long.isLong(message.messageTimestamp)
? message.messageTimestamp.toNumber()
@@ -4461,7 +4570,7 @@ export class BaileysStartupService extends ChannelStartupService {
// Use raw SQL to avoid JSON path issues
const result = await this.prismaRepository.$executeRaw`
UPDATE "Message"
UPDATE "Message"
SET "status" = ${status[4]}
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'remoteJid' = ${remoteJid}
@@ -4486,7 +4595,7 @@ export class BaileysStartupService extends ChannelStartupService {
this.prismaRepository.chat.findFirst({ where: { remoteJid } }),
// Use raw SQL to avoid JSON path issues
this.prismaRepository.$queryRaw`
SELECT COUNT(*)::int as count FROM "Message"
SELECT COUNT(*)::int as count FROM "Message"
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'remoteJid' = ${remoteJid}
AND ("key"->>'fromMe')::boolean = false
@@ -4561,8 +4670,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 +4875,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 +4905,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 { WAMessageContent, WAMessageKey } from 'baileys';
import dayjs from 'dayjs';
import FormData from 'form-data';
import { Jimp, JimpMime } from 'jimp';
@@ -44,6 +43,9 @@ 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(
@@ -568,27 +570,31 @@ 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';
const isGroup = body.key.remoteJid.endsWith('@g.us');
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
const { remoteJid } = body.key;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const 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) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
if (phoneNumber && remoteJid && !isGroup) {
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
if (contact && contact.identifier !== remoteJid) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
);
const updateContact = await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`,
identifier: remoteJid,
phone_number: `+${phoneNumber.split('@')[0]}`,
});
if (updateContact === null) {
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
const baseContact = await this.findContact(instance, phoneNumber.split('@')[0]);
if (baseContact) {
await this.mergeContacts(baseContact.id, contact.id);
this.logger.verbose(
@@ -604,7 +610,23 @@ 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: ${remoteJid}, conversation ID: ${conversationId}`);
this.logger.verbose(`Found conversation to: ${phoneNumber}, 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 +639,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 +661,7 @@ 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 : phoneNumber.split('@')[0].split(':')[0];
let nameContact = !body.key.fromMe ? body.pushName : chatId;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
@@ -653,14 +671,15 @@ export class ChatwootService {
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
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(
body.key.participant.split('@')[0],
participantJid.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
if (findParticipant) {
@@ -673,12 +692,12 @@ export class ChatwootService {
} else {
await this.createContact(
instance,
body.key.participant.split('@')[0],
participantJid.split('@')[0],
filterInbox.id,
false,
body.pushName,
picture_url.profilePictureUrl || null,
body.key.participant,
participantJid,
);
}
}
@@ -686,6 +705,7 @@ 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) {
@@ -769,7 +789,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 +822,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);
@@ -1158,7 +1178,7 @@ export class ChatwootService {
const data: SendAudioDto = {
number: number,
audio: media,
delay: 1200,
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
quoted: options?.quoted,
};
@@ -1169,7 +1189,7 @@ export class ChatwootService {
return messageSent;
}
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
type = 'document';
}
@@ -1194,6 +1214,7 @@ export class ChatwootService {
return messageSent;
} catch (error) {
this.logger.error(error);
throw error; // Re-throw para que o erro seja tratado pelo caller
}
}
@@ -1275,6 +1296,7 @@ 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({
@@ -1285,7 +1307,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 +1392,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' };
}
@@ -1417,7 +1442,6 @@ export class ChatwootService {
await this.updateChatwootMessageId(
{
...messageSent,
owner: instance.instanceName,
},
{
messageId: body.id,
@@ -1432,7 +1456,7 @@ export class ChatwootService {
const data: SendTextDto = {
number: chatId,
text: formatText,
delay: 1200,
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
quoted: await this.getQuotedMessage(body, instance),
};
@@ -1452,7 +1476,6 @@ export class ChatwootService {
await this.updateChatwootMessageId(
{
...messageSent,
instanceId: instance.instanceId,
},
{
messageId: body.id,
@@ -1483,7 +1506,7 @@ export class ChatwootService {
},
});
if (lastMessage && !lastMessage.chatwootIsRead) {
const key = lastMessage.key as ExtendedMessageKey;
const key = lastMessage.key as WAMessageKey;
waInstance?.markMessageAsRead({
readMessages: [
@@ -1520,7 +1543,7 @@ export class ChatwootService {
const data: SendTextDto = {
number: chatId,
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
delay: 1200,
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
};
sendTelemetry('/message/sendText');
@@ -1541,14 +1564,14 @@ export class ChatwootService {
chatwootMessageIds: ChatwootMessage,
instance: InstanceDto,
) {
const key = message.key as ExtendedMessageKey;
const key = message.key as WAMessageKey;
if (!chatwootMessageIds.messageId || !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},
@@ -1560,6 +1583,8 @@ export class ChatwootService {
AND "key"->>'id' = ${key.id}
`;
this.logger.verbose(`Update result: ${result} rows affected`);
if (this.isImportHistoryAvailable()) {
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
}
@@ -1609,12 +1634,13 @@ export class ChatwootService {
},
});
const key = message?.key as ExtendedMessageKey;
const key = message?.key as WAMessageKey;
const messageContent = message?.message as WAMessageContent;
if (message && key?.id) {
if (messageContent && key?.id) {
return {
key: message.key as proto.IMessageKey,
message: message.message as proto.IMessage,
key: key,
message: messageContent,
};
}
}
@@ -1913,6 +1939,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;
}
@@ -2003,7 +2030,10 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const rawPhoneNumber =
body.key.addressingMode === 'lid' && !body.key.fromMe
? body.key.participantAlt.split('@')[0]
: body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
@@ -2017,9 +2047,11 @@ export class ChatwootService {
let content: string;
if (!body.key.fromMe) {
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
content = bodyMessage
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
: `**${formattedPhoneNumber} - ${participantName}:**`;
} else {
content = `${bodyMessage}`;
content = bodyMessage || '';
}
const send = await this.sendData(
@@ -2144,7 +2176,10 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const rawPhoneNumber =
body.key.addressingMode === 'lid' && !body.key.fromMe
? body.key.participantAlt.split('@')[0]
: body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
@@ -2235,9 +2270,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 +2279,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 +2364,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) {
const msgConnection = i18next.t('cw.inbox.connected');
await this.createBotMessage(instance, msgConnection, 'incoming');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
chatwootImport.clearAll(instance);
}
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)`,
);
}
}

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}`;
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
const bindPhoneNumber = `$${bindInsert.length}`;
let bindPhoneNumber: string;
if (!isGroup) {
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
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();

View File

@@ -1,7 +1,10 @@
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[] = [];
@@ -11,6 +14,11 @@ 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 =
@@ -47,35 +55,87 @@ 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;
lid?: string;
remoteJidAlt?: string;
lid?: 'lid' | undefined;
}
export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
if (configService.get<Database>('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) {
const upsertsQuery = data.map((item) => {
for (const item of data) {
const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid;
const numbersAvailable = getAvailableNumbers(remoteJid);
return prismaRepository.isOnWhatsapp.upsert({
create: {
remoteJid: remoteJid,
jidOptions: numbersAvailable.join(','),
lid: item.lid,
// TODO: Buscar registro existente PRIMEIRO para preservar dados
const allJids = [remoteJid];
const altJid =
item.remoteJidAlt && item.remoteJidAlt.includes('@lid')
? item.remoteJidAlt.startsWith('+')
? item.remoteJidAlt.slice(1)
: item.remoteJidAlt
: null;
if (altJid) {
allJids.push(altJid);
}
const expandedJids = allJids.flatMap((jid) => getAvailableNumbers(jid));
const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({
where: {
OR: expandedJids.map((jid) => ({ jidOptions: { contains: jid } })),
},
update: {
jidOptions: numbersAvailable.join(','),
lid: item.lid,
},
where: { remoteJid: remoteJid },
});
});
await prismaRepository.$transaction(upsertsQuery);
logger.verbose(`Register exists: ${existingRecord ? existingRecord.remoteJid : 'não not found'}`);
const finalJidOptions = [...expandedJids];
if (existingRecord?.jidOptions) {
const existingJids = existingRecord.jidOptions.split(',');
// TODO: Adicionar JIDs existentes que não estão na lista atual
existingJids.forEach((jid) => {
if (!finalJidOptions.includes(jid)) {
finalJidOptions.push(jid);
}
});
}
// TODO: Se tiver remoteJidAlt com @lid novo, adicionar
if (altJid && !finalJidOptions.includes(altJid)) {
finalJidOptions.push(altJid);
}
const uniqueNumbers = Array.from(new Set(finalJidOptions));
logger.verbose(
`Saving: remoteJid=${remoteJid}, jidOptions=${uniqueNumbers.join(',')}, lid=${item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null}`,
);
if (existingRecord) {
await prismaRepository.isOnWhatsapp.update({
where: { id: existingRecord.id },
data: {
remoteJid: remoteJid,
jidOptions: uniqueNumbers.join(','),
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
},
});
} else {
await prismaRepository.isOnWhatsapp.create({
data: {
remoteJid: remoteJid,
jidOptions: uniqueNumbers.join(','),
lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null,
},
});
}
}
}
}