Compare commits

...

238 Commits
2.2.3 ... main

Author SHA1 Message Date
Davidson Gomes
39606240da Merge branch 'release/2.3.0' 2025-06-17 09:25:38 -03:00
Davidson Gomes
f4fbc4afc6 chore(changelog): remove fixed issue with @lid in chatwoot from CHANGELOG 2025-06-17 09:25:19 -03:00
Davidson Gomes
70905e7338 chore(changelog): update version date for 2.3.0 release 2025-06-17 09:20:12 -03:00
Davidson Gomes
07cccb7c7f
Merge pull request #1599 from splusoficial/develop
fix: ajuste na validacao dos bots e pausar sessao
2025-06-17 08:52:47 -03:00
Davidson Gomes
5f0862a759 test: disable webhook https test 2025-06-16 15:27:16 -03:00
Joao Vitor
029d68e2cd fix: ajuste na validacao dos bots e pausar sessao 2025-06-15 18:24:34 -03:00
Davidson Gomes
a1fae85531
Merge pull request #1594 from KokeroO/feat/improves-and-adjusts-treatment-lid-numbers
refactor(chatwoot): Melhora e ajusta tratamento de números @lid
2025-06-13 17:54:09 -03:00
Willian Coqueiro
1afa8df556 refactor(chatwoot): simplify findContact method and update contact creation logic 2025-06-13 19:36:13 +00:00
Davidson Gomes
bfb044b234 feat(whatsapp): enhance contact handling and configuration logic
- Added support for `lid` in WhatsApp numbers router to improve contact identification.
- Updated the contact retrieval logic to accept an object with `phone_number` and `identifier` for better clarity and consistency.
- Enhanced error handling and logging in the Chatwoot service for improved traceability during contact creation and updates.
- Implemented automatic search for the latest WhatsApp version if the CONFIG_SESSION_PHONE_VERSION variable is not set, ensuring users have the most up-to-date integration.
2025-06-13 14:58:01 -03:00
Davidson Gomes
ae99ec7a0e feat(whatsapp): implement fetchLatestWaWebVersion utility and update version fetching logic
- Added a new utility function `fetchLatestWaWebVersion` to retrieve the latest WhatsApp Web version.
- Updated the Baileys service and router to utilize the new function instead of the deprecated `fetchLatestBaileysVersion`, ensuring accurate version information is fetched for WhatsApp integration.
- This change enhances the reliability of version management in the application.
2025-06-13 13:14:35 -03:00
Davidson Gomes
22c379aa36 chore(env): comment out WhatsApp Web version in example configuration
- Updated the .env.example file to comment out the CONFIG_SESSION_PHONE_VERSION variable, indicating it is not currently set. This change helps clarify the configuration options available for users.
2025-06-13 12:34:45 -03:00
Davidson Gomes
62c00c3db2 feat(router): update whatsappWebVersion to fetch latest Baileys version
- Modified the root route to fetch the latest WhatsApp web version from Baileys if not set in the environment configuration.
- This enhancement ensures that clients receive the most up-to-date version information in the API response.
2025-06-13 12:03:24 -03:00
Davidson Gomes
afc2927837 feat(IsOnWhatsapp): add optional lid field and update related logic
- Introduced a new optional `lid` field in the IsOnWhatsapp model to enhance data tracking.
- Updated migration script to add the `lid` column to the database.
- Modified OnWhatsAppDto to include the `lid` property for better integration with WhatsApp user data.
- Enhanced the WhatsApp Baileys service to handle `lid` numbers separately and improve user verification logic.
- Updated cache handling functions to support the new `lid` field for consistent data management.
2025-06-13 11:52:32 -03:00
Davidson Gomes
c17b48bca0 fix(chatwoot.service): improve number retrieval logic for message senders
- Enhanced the logic for retrieving sender numbers by introducing a new method that prioritizes senderPn, followed by participant and remoteJid.
- This change ensures more reliable identification of message senders across different scenarios.
2025-06-13 09:10:29 -03:00
Davidson Gomes
534c54a171 fix(chatwoot.service): enhance contact retrieval logic and normalize identifiers
- Updated contact retrieval to handle cases where the identifier is not found, specifically for @lid scenarios.
- Introduced a new method to normalize contact identifiers, prioritizing senderLid and participantLid.
- Improved logging for better traceability during contact lookups and conversation handling.
2025-06-13 08:28:09 -03:00
Davidson Gomes
8603e6def0 feat(router): add whatsappWebVersion to response object
- Included `whatsappWebVersion` in the response object to provide the version of the WhatsApp web session from the environment configuration.
- This addition enhances the API's response with relevant session information for clients.
2025-06-12 18:08:18 -03:00
Davidson Gomes
bcf2febf48 refactor(whatsapp.business.service): replace hardcoded token with class property
- Updated the token retrieval in the BusinessStartupService to use a class property instead of a hardcoded environment variable.
- This change enhances flexibility and maintainability of the service configuration.
2025-06-12 17:32:00 -03:00
Davidson Gomes
a02ecc88f5 refactor(whatsapp.business.service): enhance media handling and audio processing
- Updated media message preparation to conditionally include filename and caption based on media type.
- Improved error handling in media ID retrieval and audio processing methods.
- Refactored audio processing to support file uploads and URL handling more effectively.
- Enhanced logging for better error tracking during media operations.
2025-06-12 17:28:02 -03:00
Davidson Gomes
bc451e8493 feat(Typebot): add splitMessages and timePerChar fields to Typebot models
- Introduced `splitMessages` and `timePerChar` fields in the Typebot and TypebotSetting models with default values.
- Created a migration script to update the database schema accordingly.
- Updated audio message handling to prepend `[audio]` to transcriptions for better clarity in message context.
2025-06-12 13:24:25 -03:00
Davidson Gomes
1eb2c848f7 fix(chatwoot.service): add isLid parameter to service methods for enhanced functionality
- Introduced a new boolean parameter `isLid` in relevant methods to improve the handling of conversation data.
- Updated conditionals to incorporate `isLid` for more precise data management.
- Ensured backward compatibility by maintaining existing functionality while enhancing the service's capabilities.
2025-06-12 12:02:38 -03:00
Davidson Gomes
7cfc359be9 chore: update package-lock.json and refactor WhatsApp Baileys service
- Added new dependency `audio-decode` version 2.2.3.
- Introduced `@eshaz/web-worker` version 1.2.2 with its metadata.
- Refactored `userDevicesCache` initialization and various object destructuring for improved readability in `whatsapp.baileys.service.ts`.
- Streamlined multiple object property assignments for better code clarity.
- Removed deprecated `@typescript-eslint/project-service` and related packages from package-lock.json.
2025-06-12 12:01:01 -03:00
Davidson Gomes
421e762c2d fix(chatwoot.service): Ensure conversation checks are robust before toggling status
- Added null checks for conversation object in reopenConversation mode and when finding unresolved conversations.
- Improved logging for better debugging of conversation retrieval.
2025-06-12 11:18:59 -03:00
Davidson Gomes
9e1f9cbb83
Merge pull request #1574 from edisonmartinsmkt/develop
feat(audio): support LPCM and fix waveform distortion
2025-06-08 20:59:46 -03:00
edisoncm-ti
44e0ff2250 feat(audio): support LPCM and fix waveform distortion 2025-06-08 20:41:59 -03:00
Davidson Gomes
614ad7cbdf
Merge pull request #1556 from splusoficial/develop
fix: adjustment in audio transcription with official api
2025-06-06 23:10:57 -03:00
Joao Vitor
77b3b331f8 fix: adjustment in audio transcription with official api 2025-06-05 20:31:11 -03:00
Davidson Gomes
e469dc132f
Merge pull request #1536 from Daquisu/fix-fetchStatusMessage-params
Update `fetchStatusMessage` to handle empty offset / page
2025-06-03 12:40:14 -03:00
Daquisu
fa14abac5a Update fetchStatusMessage to handle empty offset / page 2025-06-01 15:22:01 -06:00
Davidson Gomes
a53e0a8694
Merge pull request #1513 from samuelterra22/patch-1
fix: change service image version and change to latest config session phone version
2025-05-28 13:53:17 -03:00
Davidson Gomes
16e4bba108 chore: update dependencies in package-lock.json and package.json
This commit updates several dependencies in the package-lock.json and package.json files to their latest versions. Key changes include:
- Upgraded various @typescript-eslint packages to version 8.33.0, ensuring compatibility and access to the latest features and fixes.
- Updated the baileys package from version 6.7.17 to 6.7.18, incorporating the latest improvements.
- Bumped the pino package version from 9.6.0 to 9.7.0 for enhanced performance and stability.
- Updated the process-warning package from version 4.0.1 to 5.0.0, which may include important updates and fixes.
- Adjusted the protobufjs package version from 7.5.2 to 7.4.0, reflecting the latest changes.

These updates contribute to improved performance, security, and maintainability of the codebase.
2025-05-28 11:44:17 -03:00
Davidson Gomes
bd19fff264
Merge branch 'develop' into patch-1 2025-05-28 11:01:17 -03:00
Samuel Terra
94285ecb90
fix: change service image version and change to latest config session phone version 2025-05-28 09:22:38 -03:00
Davidson Gomes
3500fbe27f
Merge pull request #1508 from gomessguii/develop
refactor: improve chatbot integrations
2025-05-27 18:03:44 -03:00
Guilherme Gomes
cb76381466 refactor: reorder parameters and simplify EvolutionBot DTO
This commit refines the EvolutionBot integration by reordering constructor parameters for consistency and removing unused properties from the EvolutionBotDto and EvolutionBotSettingDto classes. Key changes include:
- Adjusted the parameter order in the EvolutionBotService constructor for improved clarity.
- Streamlined the EvolutionBotDto and EvolutionBotSettingDto by eliminating unnecessary fields.

These updates enhance the maintainability and readability of the EvolutionBot integration.
2025-05-27 17:52:45 -03:00
Guilherme Gomes
98502f6555 fix: resolve build errors and audio transcription issues across chatbot services
- Add YAML file loader to tsup.config.ts to fix build compilation errors
- Fix OpenAI speechToText method signature across all chatbot services
- Correct DifyService constructor parameter order in server.module.ts and channel.service.ts
- Add missing OpenAI service dependency injection to EvoaiService
- Standardize audio transcription logic in FlowiseService to match N8N implementation
- Fix speechToText calls in WhatsApp Baileys and Evolution channel services
- Ensure consistent error handling and parameter passing for audio processing

This resolves the "Cannot read properties of undefined" errors and ensures
all chatbot integrations (OpenAI, N8N, Flowise, EvoAI, Dify, EvolutionBot)
properly handle audio message transcription using OpenAI Whisper.
2025-05-27 17:46:29 -03:00
Guilherme Gomes
3fc77e4c76 refactor: update asset references in index.html and replace CSS/JS files
This commit updates the asset references in the index.html file to point to new CSS and JS files. Key changes include:
- Replaced the old JavaScript file `index-mxi8bQ4k.js` with `index-D-oOjDYe.js`.
- Updated the CSS file reference from `index-DNOCacL_.css` to the new `index-CXH2BdD4.css`.
- Removed the old CSS and JS files to clean up the codebase.

These updates ensure that the application uses the latest styles and scripts, contributing to improved performance and maintainability.
2025-05-27 17:37:01 -03:00
Guilherme Gomes
19e291178c refactor: streamline integration checks and parameter handling in chatbot controllers
This commit refines the Flowise and Typebot integrations by simplifying the integration enablement checks in their respective controllers. Key changes include:
- Consolidated the integration checks in the createBot method of FlowiseController and startBot method of TypebotController for improved readability.
- Removed unnecessary line breaks to enhance code clarity.

These updates contribute to a cleaner and more maintainable codebase for chatbot integrations.
2025-05-27 17:26:00 -03:00
Guilherme Gomes
7682a679d1 refactor: enhance Dify integration with improved validation and message processing
This commit refines the Dify integration by updating the controller and service logic for better functionality and maintainability. Key changes include:
- Added Dify-specific validation in the createBot method to prevent duplicate entries.
- Simplified comments for clarity and removed unused methods in DifyController.
- Enhanced message processing in DifyService to handle audio messages more effectively and improve error handling.
- Updated DifyDto and DifySettingDto to streamline properties and improve clarity.

These updates contribute to a more robust and maintainable Dify integration.
2025-05-27 17:04:35 -03:00
Guilherme Gomes
97ca23a7b0 refactor: enhance Evoai integration with improved validation and message handling
This commit refines the Evoai integration by updating the service and controller logic for better functionality and maintainability. Key changes include:
- Added the `openaiService` as a parameter in the EvoaiService constructor for improved dependency management.
- Enhanced the createBot method in EvoaiController to include EvoAI-specific validation and duplicate checks.
- Updated EvoaiDto and EvoaiSettingDto to remove unnecessary comments and add a fallback property.
- Refined the message processing logic in EvoaiService to handle audio messages more effectively and improve logging clarity.
- Adjusted the schema for Evoai settings to rename `evoaiIdFallback` to `botIdFallback` for better clarity.

These updates contribute to a more robust and maintainable Evoai integration.
2025-05-27 16:00:32 -03:00
Guilherme Gomes
95bd85b6e3 refactor: update Flowise integration for improved configuration and validation
This commit refines the Flowise integration by enhancing configuration management and validation logic. Key changes include:
- Reordered parameters in the FlowiseService constructor for consistency.
- Updated FlowiseController to utilize the configService for integration enablement checks.
- Simplified FlowiseDto and FlowiseSettingDto by removing unused properties.
- Enhanced validation logic in flowise.schema.ts to include new fields.
- Improved error handling in the createBot method to prevent duplicate entries.

These updates contribute to a more robust and maintainable Flowise integration.
2025-05-27 15:49:15 -03:00
Guilherme Gomes
64fc7a05ac refactor: improve session handling and validation in N8n integration
This commit enhances the N8n integration by refining session management and validation logic. Key changes include:
- Added error handling for session creation failures in the BaseChatbotService.
- Removed unused methods and properties in N8nService and N8nDto to streamline the codebase.
- Updated N8n schema to enforce required fields and improve validation checks.
- Simplified message processing logic to utilize base class methods, enhancing maintainability.

These improvements contribute to a more robust and efficient N8n integration.
2025-05-27 15:10:47 -03:00
Guilherme Gomes
39aaf29d54 refactor: enhance OpenAI controller and service for better credential management
This commit refactors the OpenAIController and OpenAIService to improve credential handling and error management. Key changes include:
- Added checks to prevent duplicate API keys and names during bot creation.
- Updated the getModels method to accept an optional credential ID for more flexible credential usage.
- Enhanced error handling with specific BadRequestException messages for better clarity.
- Removed unused methods and streamlined the speech-to-text functionality to utilize instance-specific settings.

These improvements enhance the maintainability and usability of the OpenAI integration.
2025-05-27 14:32:10 -03:00
Guilherme Gomes
22e99f7934 refactor: simplify TypebotController and TypebotService methods
This commit refactors the TypebotController and TypebotService to streamline the processTypebot method, aligning it with the base class pattern. It reduces complexity by consolidating parameters and improving readability. Additionally, it updates the TypebotDto and TypebotSettingDto to remove unused properties, enhancing code clarity and maintainability.
2025-05-27 13:07:15 -03:00
Davidson Gomes
dd0dfd447c
Merge pull request #1504 from KokeroO/develop
fix: Melhora o método createConversation (evita conversas criadas duplicadas Chatwoot)
2025-05-27 08:11:34 -03:00
Willian Coqueiro
fb18267ac5 fix: eslint 2025-05-27 02:10:15 +00:00
Willian Coqueiro
fc5965938e fix: improve createConversation method with caching and locking mechanisms 2025-05-27 01:31:06 +00:00
Davidson Gomes
623efd86a2
Merge pull request #1498 from thrsouza/main
Inclusão do parâmetro frame_max para compatibilidade com RabbitMQ 4.1+
2025-05-25 11:10:40 -03:00
Thiago Souza
3297364c10 fix: update RabbitMQ frame_max parameter for 4.1+ compatibility
Updates the minimum frame_max value from 4096 to 8192 to meet the requirements
of RabbitMQ 4.1+ servers. This resolves connection failures with newer RabbitMQ
versions while maintaining backwards compatibility with older versions.
2025-05-25 03:08:12 -03:00
Davidson Gomes
373a531e88 fix: update logging and message handling in EvoaiService
This commit modifies the logging messages in the `EvoaiService` to use the correct service name "EvoAI" instead of "Dify" for better clarity. Additionally, it refines the message handling logic by changing the way message IDs are generated and updating the payload structure sent to the API. The extraction of the message from the response artifacts has also been improved to ensure that the correct message is retrieved from the response data.

Changes include:
- Updated logging statements to reflect the correct service name.
- Changed the message ID generation to use a shorter UUID substring.
- Adjusted the payload structure to include `contextId` and `messageId`.
- Enhanced message extraction logic from the response artifacts.

These changes enhance the clarity of logs and improve the robustness of message handling in the service.
2025-05-23 20:37:01 -03:00
Davidson Gomes
e081533f02 fix: remove unused Auth import from Dify and N8n services
This commit removes the unused `Auth` import from both `dify.service.ts` and `n8n.service.ts`. This change helps to clean up the code and improve readability by eliminating unnecessary dependencies.
2025-05-23 20:09:36 -03:00
Davidson Gomes
17fd407d8d
Merge pull request #1494 from KokeroO/develop
fix: melhora a formatação e tratamento de erros na função getExistingSourceIds
2025-05-23 10:43:27 -03:00
Willian Coqueiro
0cdc67effe fix: eslint 2025-05-23 02:40:05 +00:00
Willian Coqueiro
ddaf32be76 fix: melhora a formatação e tratamento de erros na função getExistingSourceIds 2025-05-23 02:18:13 +00:00
Willian Coqueiro
0b2d8a752f fix: melhora o tratamento de erros e otimiza a consulta de IDs de origem no Chatwoot 2025-05-23 02:13:46 +00:00
Davidson Gomes
9cda6a2f99
Merge pull request #1493 from oriondesign2015/develop
corrige estrutura de if/else e bloco try/catch em chatwoot-import-helper.ts
2025-05-22 21:57:33 -03:00
OrionDesign
d2263af3e8 fix: corrige estrutura de if/else e bloco try/catch em chatwoot-import-helper.ts
Esta PR corrige problemas de sintaxe no arquivo chatwoot-import-helper.ts, especificamente:
- Ajusta a estrutura do bloco if/else no método getExistingSourceIds, evitando erro de compilação do TypeScript.
- Garante que o bloco catch esteja corretamente posicionado dentro do método, retornando null em caso de erro.
- Mantém a lógica original do método, apenas corrigindo a sintaxe para permitir a compilação e execução correta do projeto.
Essas correções eliminam o erro de build relacionado ao TypeScript e melhoram a robustez do código ao tratar exceções de forma adequada.
2025-05-22 18:51:28 -03:00
Davidson Gomes
0239638232
Merge pull request #1490 from KokeroO/develop
fix: Corrige o PR1481
2025-05-22 17:08:43 -03:00
Willian Coqueiro
3459d61eff fix: Corrige o PR1481 2025-05-22 20:03:32 +00:00
Davidson Gomes
5330121c49
Merge pull request #1486 from oriondesign2015/develop
fix: Corrige envio da apiKey da instância nos payloads do Flowise e do Dify
2025-05-22 11:24:20 -03:00
OrionDesign
53c1c218c4 fix: Corrige envio da apiKey da instância no payload do Dify
Corrige o envio da apiKey no payload do Dify para usar a apiKey específica da instância ao invés da apiKey global do sistema.
2025-05-22 11:04:13 -03:00
OrionDesign
06378e5d6b fix: Corrige envio da apiKey da instância no payload do Flowise
Corrige o envio da apiKey no payload do Flowise para usar a apiKey específica da instância ao invés da apiKey global do sistema.
2025-05-22 11:03:22 -03:00
Davidson Gomes
6a83e89394
Merge pull request #1485 from oriondesign2015/develop
fix: Corrige envio da apiKey da instância nos payloads do Evolution Bot e N8N
2025-05-22 11:02:21 -03:00
OrionDesign
d24540d6dd fix: Corrige envio da apiKey da instância no payload do N8N
Corrige o envio da apiKey no payload do N8N para usar a apiKey específica da instância ao invés da apiKey global do sistema.
2025-05-22 10:57:41 -03:00
OrionDesign
2af7b24013 fix: Corrige envio da apiKey da instância no payload do Evolution Bot
Corrige o envio da apiKey no payload do Evolution Bot para usar a apiKey específica da instância ao invés da apiKey global do sistema.
2025-05-22 10:56:54 -03:00
Davidson Gomes
6f47a54fae
Merge pull request #1483 from KokeroO/develop
chore: possibilita o envio de medias do tipo [svg, tiff] vindas do Chatwoot
2025-05-22 07:03:41 -03:00
Davidson Gomes
edde059fa1
Merge pull request #1484 from oriondesign2015/develop
fix: melhora consistência e formatação dos chatbots (N8N e Evolution Bot)
2025-05-22 06:58:21 -03:00
OrionDesign
dcb09b87fe fix: corrige comportamento de sessão pausada no N8N
Corrige o problema onde o N8N reativava automaticamente a sessão após receber uma mensagem quando estava pausado. Agora, quando uma sessão está pausada, o bot ignora completamente as mensagens recebidas até que a sessão seja explicitamente reativada através do endpoint de mudança de status.
2025-05-22 01:08:48 -03:00
OrionDesign
bbf142cf39 fix: corrige comportamento de sessão pausada no Evolution Bot
Corrige o problema onde o Evolution Bot reativava automaticamente por qualquer mensagem do usuario quando a sessão estava pausada. Agora, quando uma sessão está pausada, o bot ignora completamente as mensagens recebidas até que a sessão seja explicitamente reativada.
2025-05-22 01:01:51 -03:00
OrionDesign
da51b6bd76 fix: corrige processamento de mensagens subsequentes no Evolution Bot
Corrige o problema onde o Evolution Bot não processava mensagens subsequentes após a primeira resposta. A correção permite que o bot continue respondendo a todas as mensagens enquanto a sessão estiver ativa, melhorando a continuidade da conversa.
2025-05-22 00:54:37 -03:00
Willian Coqueiro
8b15c11817 chore: adiciona suporte para extensão de arquivo .tif no envio de documentos 2025-05-22 03:38:57 +00:00
Willian Coqueiro
272a4de236 chore: possibilita o envio de medias do tipo svg e tiff vindas do Chatwoot 2025-05-22 03:29:16 +00:00
OrionDesign
65111481b9 fix: remove quebras de linha extras nas mensagens do N8n
Corrige o problema de formatação nas mensagens do N8n onde quebras de linha extras estavam sendo adicionadas antes e depois das mídias (imagens, vídeos, etc). Agora o texto é enviado mantendo apenas as quebras de linha intencionais.
2025-05-22 00:17:06 -03:00
Willian Coqueiro
0ca109e9d6 chore: possibilita o envio de medias do tipo .svg 2025-05-22 03:02:04 +00:00
Davidson Gomes
5b817028a9 refactor(chatbot): enhance EvoaiService with OpenAI integration
- Integrated OpenAI service into the EvoaiService to streamline audio message transcription.
- Updated the constructor to initialize OpenaiService, allowing for direct transcription of audio messages.
- Refactored audio message handling to utilize the OpenAI service for improved reliability and maintainability.
- Adjusted the processing logic to handle transcription results more effectively, ensuring fallback content is provided when transcription fails.

This commit focuses on enhancing the EvoaiService's capabilities by leveraging OpenAI for audio transcription, improving overall service functionality and code structure.
2025-05-21 22:27:59 -03:00
Davidson Gomes
6a0fc19702 refactor(chatbot): integrate OpenAI service into chatbot implementations
- Updated various chatbot services (Typebot, Dify, EvolutionBot, Flowise, N8n) to include the OpenAI service for audio transcription capabilities.
- Modified constructors to accept OpenaiService as a dependency, enhancing the ability to transcribe audio messages directly within each service.
- Refactored the handling of `keywordFinish` in multiple controllers and services, changing its type from an array to a string for consistency and simplifying logic.
- Removed redundant audio transcription logic from the base service, centralizing it within the OpenAI service to improve maintainability and reduce code duplication.

This commit focuses on enhancing the chatbot services by integrating OpenAI's transcription capabilities, improving code structure, and ensuring consistent handling of session keywords.
2025-05-21 22:17:10 -03:00
Davidson Gomes
9cedf31eed feat(env): enhance webhook configuration and SSL support
- Added new environment variables for SSL configuration, including `SSL_CONF_PRIVKEY` and `SSL_CONF_FULLCHAIN`, to support secure connections.
- Introduced additional webhook configuration options in the `.env.example` file, such as `WEBHOOK_REQUEST_TIMEOUT_MS`, `WEBHOOK_RETRY_MAX_ATTEMPTS`, and related retry settings to improve webhook resilience and error handling.
- Updated the `bootstrap` function in `main.ts` to handle SSL certificate loading failures gracefully, falling back to HTTP if necessary.
- Enhanced error handling and logging in the `BusinessStartupService` to ensure better traceability and robustness when processing messages.

This commit focuses on improving the security and reliability of webhook interactions while ensuring that the application can handle SSL configurations effectively.
2025-05-21 17:55:00 -03:00
Davidson Gomes
f9567fbeaa refactor(chatbot): unify keywordFinish type and enhance session handling
- Changed the type of `keywordFinish` from an array to a string in multiple DTOs and controller interfaces to simplify data handling.
- Updated the `BaseChatbotService` to include logic for updating session status to 'opened' and managing user responses more effectively.
- Refactored the media message handling in the `BaseChatbotService` to streamline the process and improve readability.
- Enhanced error logging across various services to ensure better traceability during operations.

This commit focuses on improving the structure and consistency of chatbot integrations while ensuring that session management is robust and user-friendly.
2025-05-21 17:02:24 -03:00
Davidson Gomes
d673c83a93 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2025-05-21 15:14:50 -03:00
Davidson Gomes
e16bb0e580 style(whatsapp): format code for consistency
- Adjusted spacing in destructuring assignments for `remoteJid` in multiple locations to enhance code readability and maintain consistency with coding standards.
- Removed unnecessary blank lines to streamline the code structure.

This commit focuses on improving the overall style of the `whatsapp.baileys.service.ts` file without altering any functionality.
2025-05-21 15:14:41 -03:00
Davidson Gomes
09120aa026
Merge pull request #1482 from gomessguii/fix/message-query
Refatoração da funcionalidade de chatbots (em andamento)
2025-05-21 15:14:25 -03:00
Davidson Gomes
fbfa364df9 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2025-05-21 15:01:22 -03:00
Davidson Gomes
624b37e2aa fix(media): improve media download handling for base64 conversion
- Enhanced the logic for converting media messages to base64 by adding a retry mechanism for downloading media if the initial buffer is not available.
- This change ensures that media messages are reliably converted to base64 format, improving the robustness of media handling in the WhatsApp integration service.
- Updated error logging to capture issues during the media conversion process.
2025-05-21 15:01:17 -03:00
Davidson Gomes
fcde1f9acc
Merge pull request #1481 from AlexSzefezuk/fix-chatwoot-import
Fix-chatwoot-import
2025-05-21 13:54:51 -03:00
Davidson Gomes
cc01787501
Merge pull request #1473 from KokeroO/develop
fix: Deadlock errors and undefined arguments
2025-05-21 13:52:57 -03:00
Guilherme Gomes
c30bae4c3a refactor(openai): improve service initialization and streamline audio transcription handling
- Updated OpenaiService and related classes to enhance the initialization process by ensuring the correct order of parameters.
- Simplified audio message handling by consolidating transcription logic and improving error handling.
- Refactored the OpenaiController to utilize the new structure, ensuring better integration with the base chatbot framework.
- Enhanced logging for better traceability during audio processing and API interactions.
2025-05-21 12:16:12 -03:00
Alex Szefezuk
2545013040 fix: enable sourceId exists in a conversation 2025-05-21 10:36:26 -03:00
Willian Coqueiro
c53a96e757 Fix suggestions 2025-05-20 13:07:28 +00:00
Willian Coqueiro
a2d8642e1c fix: Corrige processamento de documentos sem filename.
## Erros:
- Cannot read properties of null (reading 'fileName')
2025-05-19 23:21:01 +00:00
Willian Coqueiro
9c530c69cf fix: Evita tentar processar media messageContextInfo e disparar um erro generico. 2025-05-19 23:18:23 +00:00
Willian Coqueiro
348a4ff251 fix: Corrige problemas com deadlocks
## Erros:
- code: "40P01", message: "deadlock detected".
- Argument status is missing.
2025-05-19 23:14:57 +00:00
Willian Coqueiro
fc00916345 fix: Corrige o erro Unable to fit integer value '1747658857155' into an INT4 (32-bit signed integer.
## Solução
Após analise dos valores recebidos e seguindo a logica em outras partes também, se confirma que o valor de entrada é de timestamp em ms.
2025-05-19 23:05:11 +00:00
Guilherme Gomes
69b4f1aa02 feat(chatbot): implement base chatbot structure and enhance integration capabilities
- Introduced a base structure for chatbot integrations, including BaseChatbotController and BaseChatbotService.
- Added common DTOs for chatbot settings and data to streamline integration processes.
- Updated existing chatbot controllers (Dify, Evoai, N8n) to extend from the new base classes, improving code reusability and maintainability.
- Enhanced media message handling across integrations, including audio transcription capabilities using OpenAI's Whisper API.
- Refactored service methods to accommodate new message structures and improve error handling.
2025-05-17 16:22:13 -03:00
Davidson Gomes
7cccda10bb
Merge pull request #1456 from thiagoomatheus/main
Fixes issue #879
2025-05-16 13:38:30 -03:00
Davidson Gomes
33c808b195
Merge pull request #1457 from gomessguii/fix/message-query
feat(channel): enhance pushName logic for messages
2025-05-16 12:02:31 -03:00
Guilherme Gomes
d3ee370bdc feat(channel): enhance pushName logic for messages
- Updated the pushName selection to differentiate between group and individual messages.
- Added conditional logic to display the chat name for group messages and the sender's name for individual messages.
2025-05-16 11:33:52 -03:00
thiagoomatheus
7e5740b462 fix(media): allow multiples files with same name 2025-05-15 22:37:55 -03:00
Davidson Gomes
0b33a76394 changelog v2.3.0 2025-05-15 18:49:35 -03:00
Davidson Gomes
6a0b024b13
Merge pull request #1453 from gomessguii/fix/evoai-migration
feat(evoai): add Evoai and EvoaiSetting tables with foreign key const…
2025-05-15 18:20:09 -03:00
Guilherme Gomes
fda6b0d50e feat(evoai): add Evoai and EvoaiSetting tables with foreign key constraints
- Created the Evoai and EvoaiSetting tables in the PostgreSQL migration.
- Defined primary keys and added foreign key constraints to link with the Instance table.
- Included unique index on instanceId for EvoaiSetting to ensure data integrity.
2025-05-15 18:19:23 -03:00
Davidson Gomes
9ec6847683
Merge pull request #1452 from gomessguii/feature/new-manager-version
feat: updated manager to the last version with suport to n8n and EvoA…
2025-05-15 18:12:38 -03:00
Guilherme Gomes
c745fbad64 feat: updated manager to the last version with suport to n8n and EvoAI chatbot integrations 2025-05-15 18:09:23 -03:00
Davidson Gomes
40ea8bf356
Merge pull request #1451 from gomessguii/feature/evoai-chatbot
feat(evoai): add EvoAI integration with models, services, and routes
2025-05-15 15:50:12 -03:00
Guilherme Gomes
0699ad4bb0 fix(evoai): update EvoAI service initialization to include configService
- Modified the EvoaiService instantiation in the server module to include configService for enhanced configuration management.
2025-05-15 15:46:13 -03:00
Guilherme Gomes
70a4fe8f6e feat(evoai): enhance media message handling and transcription capabilities
- Added support for audio message detection and transcription using OpenAI's Whisper API.
- Integrated media downloading for both audio and image messages, with appropriate error handling.
- Updated logging to redact sensitive information from payloads.
- Modified existing methods to accommodate the new message structure, ensuring seamless integration with EvoAI services.
2025-05-15 15:42:17 -03:00
Guilherme Gomes
71124755b0 feat(evoai): add EvoAI integration with models, services, and routes
- Introduced Evoai and EvoaiSetting models in both MySQL and PostgreSQL schemas.
- Implemented EvoaiController and EvoaiService for managing EvoAI bots.
- Created EvoaiRouter for handling API requests related to EvoAI.
- Added DTOs and validation schemas for EvoAI integration.
- Updated server module and chatbot controller to include EvoAI functionality.
- Configured environment settings for EvoAI integration.
2025-05-15 11:54:11 -03:00
Davidson Gomes
a1cc504777
Merge pull request #1449 from gomessguii/feature/enhance-message-fetching
feat(whatsapp): enhance message fetching and processing logic
2025-05-14 21:44:39 -03:00
Davidson Gomes
0fd2e04286
Merge pull request #1448 from gomessguii/feature/n8n-chatbot
Add N8n integration with models, services, and routes
2025-05-14 21:44:25 -03:00
Guilherme Gomes
4f2b0c42f3
Merge pull request #2 from EvolutionAPI/develop
Develop
2025-05-14 21:35:36 -03:00
Guilherme Gomes
401b0359cd
Merge pull request #1 from EvolutionAPI/develop
Develop
2025-05-14 21:32:41 -03:00
Davidson Gomes
b1f3c5cc5f
Merge pull request #1450 from gomessguii/fix/npm-install-action
fix: remove postinstall script from package.json to fix GitHub Action
2025-05-14 21:29:57 -03:00
Guilherme Gomes
362736ea71 refactor(whatsapp): format userDevicesCache initialization for improved readability 2025-05-14 21:27:23 -03:00
Guilherme Gomes
2544c10592 fix: remove postinstall script from package.json to fix GitHub Action 2025-05-14 21:22:52 -03:00
Guilherme Gomes
383805aa95 feat(whatsapp): enhance message fetching and processing logic
- Added a new method `fetchMessages` to retrieve messages based on various query parameters.
- Improved handling of `pushName` for messages, ensuring proper assignment based on participant information.
- Refactored user devices cache initialization for better readability.
- Cleaned up commented-out code related to message recovery.
2025-05-14 21:12:45 -03:00
Guilherme Gomes
71101807bb Refactor N8n integration: update schema exports, improve import order, and enhance service logic
- Added export for N8n schema in chatbot.schema.ts.
- Improved import order in n8n.dto.ts and n8n.router.ts for better readability.
- Refactored variable declarations in n8n.service.ts for consistency and clarity.
2025-05-14 20:50:48 -03:00
Guilherme Gomes
38f089f04c Add N8n integration with models, services, and routes
- Introduced N8n and N8nSetting models in both MySQL and PostgreSQL schemas.
- Implemented N8nController and N8nService for managing N8n bots.
- Created N8nRouter for handling API requests related to N8n.
- Added DTOs and validation schemas for N8n integration.
- Updated server module and chatbot controller to include N8n functionality.
- Configured environment settings for N8n integration.
2025-05-14 20:47:23 -03:00
Davidson Gomes
6d63f2fb6e
Merge pull request #1420 from luiis716/main
Findchat group name treatment
2025-05-13 06:28:21 -03:00
Davidson Gomes
bb0b9b94ff
Merge branch 'develop' into main 2025-05-13 06:28:07 -03:00
luiis716
a449fdf0ef
Findchat group name treatment 2025-05-12 16:14:59 -03:00
Davidson Gomes
ec9de49647
Merge pull request #1435 from edisonmartinsmkt/fix/audio-caption-fallback
fix(chatwoot): only fallback audio caption when audioMessage exists
2025-05-10 19:14:41 -03:00
edisoncm-ti
a7a9de2903 fix: only fallback caption when audioMessage exists 2025-05-10 16:20:05 -03:00
Davidson Gomes
99b0c86278
Merge pull request #1422 from icaro-andrade/patch-2
Update package.json
2025-05-10 13:07:39 -03:00
Davidson Gomes
e6a72bd829
Merge pull request #1406 from Faelst/hotfix/issues-1385
fix: when you set jpegThumbnail, image not appear on app mobile
2025-05-10 13:07:06 -03:00
Davidson Gomes
341a0d884f
Merge pull request #1332 from theeusmartins/main
Defininando TTL no userDivicesCache igual usado no Baileys
2025-05-10 11:26:23 -03:00
Davidson Gomes
383bac090a
Merge pull request #1434 from edisonmartinsmkt/fix/group-audio-caption
fix(chatwoot): avoid "undefined" caption on group audio messages
2025-05-10 11:25:14 -03:00
edisoncm-ti
60a58ca037 fix: not show undefined in caption for received audio messages in group chats 2025-05-10 11:12:44 -03:00
Davidson Gomes
e5989f3d47
Merge pull request #1433 from paulocoutinhox/fix-mysql-wavoip-token
fix mysql wavoip token
2025-05-10 10:32:12 -03:00
Davidson Gomes
bff3bf564b
Merge pull request #1415 from victoreduardo/victoreduardos/fix-conversation-not-found
fix: Erro na criação de conversation quando já existe uma conversation de outro inbox para o mesmo usuário
2025-05-10 10:31:06 -03:00
Davidson Gomes
c74eee8e52
Merge pull request #1414 from onerrogus/fix_link_preview
Corrigir utilização do linkPreview no WhatsApp Business API
2025-05-10 10:30:54 -03:00
Davidson Gomes
d1a28ea4f7
Merge pull request #1318 from victoreduardo/victoreduardos/jwt-webhook
Tornando Webhook mais seguro com JWT token
2025-05-10 10:28:07 -03:00
Davidson Gomes
d2f1985913
Merge pull request #1425 from edisonmartinsmkt/develop
fix(audio): ensure full WhatsApp compatibility for audio conversion
2025-05-10 08:33:28 -03:00
Paulo Coutinho
90e27cc7d8 fix mysql wavoip token 2025-05-10 03:53:35 -03:00
edisoncm-ti
c4ddfe6804 style: fix linting issues with Prettier 2025-05-07 18:18:47 -03:00
edisoncm-ti
aaa103a842 fix(audio): ensure full WhatsApp compatibility for audio conversion (libopus, 48kHz, mono) 2025-05-07 13:41:09 -03:00
icaro-andrade
b2809b6f3e
Update package.json 2025-05-06 19:33:46 -03:00
luiis716
d52256718d
Findchat group name treatment
ajusta para endpoint puxa o nome do grupo correntemente quando for grupo usando nomenclatura @g.us
2025-05-03 14:55:07 -03:00
Victor Eduardo
8f0ede4207 fix: preventing use conversation from other inbox for the same user 2025-05-03 09:56:28 -03:00
Victor Eduardo
95827a2d70 lint 2025-05-03 09:42:18 -03:00
Victor Eduardo
b064e512e2
Merge branch 'develop' into victoreduardos/jwt-webhook 2025-05-03 09:40:37 -03:00
OnerRogus
db5f0d0891 fix: corrigido erro que não exibia o preview das urls quando utilizado o whatsapp business api 2025-05-02 15:32:47 -03:00
Davidson Gomes
ccbd866e42
Merge pull request #1384 from leandrosroc/develop
fix(api): modifica fetchChats para trazer mensagens de contatos não salvos
2025-04-30 06:15:42 -03:00
Rafael Silverio
dc67039b39 fix: when you set jpegThumbnail, image not appear on app mobile 2025-04-29 14:54:42 -03:00
Leandro Santos Rocha
eeedfb0e2a
fetchContacts - nestordavalos 2025-04-22 16:12:47 -03:00
Leandro Santos Rocha
3ab75faff7
fix lint 2025-04-21 00:42:33 -03:00
Leandro Santos Rocha
095754d173
perf(api): otimiza paginação em fetchChats usando LIMIT/OFFSET no SQL 2025-04-21 00:38:25 -03:00
Leandro Rocha
b94b452597 fix(api): modifica fetchChats para trazer mensagens de contatos não salvos
- Muda tabela base da consulta de Contact para Message
- Altera INNER JOIN para LEFT JOIN entre Message e Contact
- Usa COALESCE para campos que podem estar vazios
- Adiciona flag isSaved para identificar contatos salvos/não salvos
- Preserva toda funcionalidade de filtros existente

Resolve issue #1376
2025-04-21 00:30:48 -03:00
Davidson Gomes
2ded19752f
Merge pull request #1366 from adaptwebtech/hotfix_chatname
Corrigindo um bug ao atualizar o push name no evento MESSAGES_UPSERT e MESSAGES_UPDATE
2025-04-14 20:06:23 -03:00
pedro-php
4c8c7ee19b lint fixes 2025-04-09 15:15:48 -03:00
pedro-php
8c6f95fbef Fixing chatname on the events message.upsert and message.update in order to return always the chatname from the user correctly 2025-04-09 14:55:22 -03:00
Davidson Gomes
402b37d7b4
Merge pull request #1351 from ricocorreia1/main
Correção para quando enviar uma localização.
2025-04-08 19:10:38 -03:00
Davidson Gomes
09f79c94be
Merge pull request #1362 from Deyvi-dev/develop
feat(s3): add S3_SKIP_POLICY env variable to disable setBucketPolicy
2025-04-08 10:51:52 -03:00
Davidson Gomes
7c7dca9da9
Merge pull request #1354 from jeffersonfelixdev/hotfix/issue-1348
Hotfix - shell injection vulnerability
2025-04-08 10:51:35 -03:00
deyvi-dev
1d2e029b54 feat(s3): add S3_SKIP_POLICY env variable to disable setBucketPolicy for incompatible providers 2025-04-07 20:23:33 -03:00
Jefferson Felix
3f8d89e970 fix: remove wildcard 2025-04-02 12:04:07 -03:00
Jefferson Felix
abda9e2113 docs: update CHANGELOG.md 2025-04-02 11:55:53 -03:00
Jefferson Felix
2a020928e8 fix: change execSync to execFileSync 2025-04-02 11:55:23 -03:00
ricocorreia1
b436e5b0b0 quando localização, conversão de degreesLatitude para string, o parametro content é do tipo string nas chamadas de findBotByTrigger() em chatbot.controller. 2025-04-01 23:26:46 -03:00
Davidson Gomes
9889035ddc
Merge pull request #1344 from alvestassio/fix/multiple-files-sent-via-whatsapp
Adicionando um timestamp ao filename para possibilitar enviar o mesmo arquivo mais de uma vez na mesma conversa.
2025-03-30 12:09:01 -03:00
Davidson Gomes
07ce09d8e2
Merge pull request #1322 from marceloapd/fix/animated-stickers
Fix/animated stickers
2025-03-30 12:08:15 -03:00
Davidson Gomes
0d2a7ad50b
Merge pull request #1343 from adaptwebtech/new_feature_add_send_update_message_webhook
Adicionando um novo webhook no endpoint de updateMessage
2025-03-30 12:08:03 -03:00
pedro-php
7e8044a777 lint changes 2025-03-28 11:10:48 -03:00
pedro-php
645f305cd6 fixing build error on prisma 2025-03-28 11:08:38 -03:00
pedro-php
829032dc08 lint changes 2025-03-28 10:58:32 -03:00
pedro-php
3d40b0850b lint changes 2025-03-28 10:57:59 -03:00
pedro-php
17bd108251 treating errors gracefully 2025-03-28 10:56:19 -03:00
pedro-php
f695e8bdc9 merging with develop 2025-03-28 10:36:34 -03:00
Tassio Alves
bf59ff1287 [FIX] Run lint 2025-03-27 14:36:39 -04:00
Tassio Alves
ce1680f515 [FIX] Adding a timestamp to the filename to make it possible to send the same file more than once in the same conversation. 2025-03-27 10:46:13 -04:00
pedro-php
119ceba1ca Adding a new webhook that triggers when a message is updated by the user 2025-03-27 11:13:22 -03:00
Marcelo Assis
9710fbdac4 remove animated to webp 2025-03-26 13:09:08 -03:00
Marcelo Assis
658dae0b59 lint fix 2025-03-26 10:45:09 -03:00
Davidson Gomes
b89f1144b4
Merge pull request #1334 from adaptwebtech/fix_and_add_name_to_find_chats_and_paginate_get_contacts_and_get_chats
Corrigindo um bug no endpoint de findChats e permitindo paginação nos endpoints de findChats e findContacts
2025-03-26 10:34:56 -03:00
Pedro Afonso
d196590862
Update src/api/services/channel.service.ts
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-03-25 11:17:46 -03:00
pedro-php
c5c354ffe7 fix_and_add_name_to_find_chats_and_paginate_get_contacts_and_get_chats 2025-03-25 10:52:19 -03:00
Matheus Martins Piedade
36757dddc2 Defininando TTL no userDivicesCache igual usado no Baileys 2025-03-24 15:15:45 -03:00
Marcelo Assis
027401b839 fix: normalize file extension checks for case insensitivity in sticker conversion 2025-03-20 19:13:19 -03:00
Marcelo Assis
6e7dd51679 fix: preserve animation in GIF and WebP stickers 2025-03-20 19:00:02 -03:00
Marcelo Assis
f8b1c6e0fa fix: preserve animation in GIF and WebP stickers 2025-03-20 18:37:58 -03:00
Victor Eduardo
cee2bc4d71 sending JWT token when sending webhook if jwt_key exists in webhook header record 2025-03-19 18:04:42 -03:00
Davidson Gomes
043df62a8f
Merge pull request #1304 from rafwell/main
Add eventos referente a instancia que estavam faltando
2025-03-14 14:33:41 -03:00
Rafael Souza
53ae521863 Add eventos referente a instancia que estavam faltando 2025-03-10 18:11:03 -03:00
Davidson Gomes
13bdbc268c
Merge pull request #1290 from jrCleber/main
Corrige validação de URL para permitir localhost e endereços IP
2025-03-06 16:59:48 -03:00
Davidson Gomes
ea9c3fbbe0
Merge pull request #1287 from pedroepif/main
fix: adjusting cloud api send audio and video
2025-03-06 16:53:05 -03:00
Cleber Wilson
22a958616d basic regex for url 2025-03-06 16:17:42 -03:00
Davidson Gomes
32cde710b8 feat: Enhance WebSocket authentication and connection handling
- Add robust authentication mechanism for WebSocket connections
- Implement API key validation for both instance-specific and global tokens
- Improve connection request handling with detailed logging
- Refactor WebSocket controller to support more secure connection validation
2025-03-06 09:17:43 -03:00
Pedro Epifanio
d9aa111800 fix: lint 2025-03-06 08:59:45 -03:00
Pedro Epifanio
fb41ab14e8 fix: adding media verification 2025-03-05 20:01:14 -03:00
Pedro Epifanio
5998fcf940 fix: adjustin cloud api send audio and video 2025-03-05 19:54:34 -03:00
Davidson Gomes
2198a86ae4 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2025-02-25 15:44:42 -03:00
Davidson Gomes
8a54efe11c feat: Add NATS integration and update Baileys service
- Create Nats table in PostgreSQL migration
- Disable message recovery logic in Baileys service
- Remove console log in instance creation route
2025-02-25 15:42:40 -03:00
Davidson Gomes
4fadf64bae
Merge pull request #1264 from ygorsantana/fix/expiration-useless
Fix: Expiration being useless on awaitUser false
2025-02-22 21:03:05 -03:00
Ygor Santana
2f1df734e0 style: run lint 2025-02-22 14:17:37 -03:00
Ygor Santana
247c39fe39 fix: expiration time being useless when not awaitUser 2025-02-22 14:16:08 -03:00
Davidson Gomes
ada63b58af
Merge pull request #1251 from fernandoralha/hotfix/sqs-event
fix: Refactor SQS controller to correct bug in sqs events by instance
2025-02-21 19:31:29 -03:00
Davidson Gomes
5192f49a4f
Merge pull request #1259 from julianoaj/develop
 Feat: Remover a reação de uma mensagem.
2025-02-21 19:24:39 -03:00
julianoaj
48b5fd41e0 🐛 Fix: Linting requirements 2025-02-20 17:25:39 -03:00
julianoaj
5720bdc0ef Remove reaction from a message 2025-02-20 16:54:29 -03:00
Alison Juliano
ab2364b9a3
Merge pull request #1 from julianoaj/fix-1234-migration-wavoipToken-create
🐛 Corrige problema na API relacionado à migration. Fixes #1234
2025-02-18 13:36:21 -03:00
“fernandoralha”
278add6b11 fix: Refactor SQS controller to correct bug in sqs events by instance
- Implement dynamic queue creation based on enabled events - Add method to list existing queues for an instance - Improve error handling and logging for SQS operations - Remove unused queue removal methods - Update set method to handle queue creation/deletion on event changes - Add comments for future feature of forced queue deletion
2025-02-17 21:22:59 -03:00
Davidson Gomes
8f632a6f5c
Merge pull request #1244 from AndersonSilvaCavalcante/hotfix/missing-wavoipToken-mysql
hotfix(migration): add missing wavoipToken column in MySQL schema
2025-02-16 13:04:29 -03:00
Anderson Cavalcante
a4ac798b43 hotfix(migration): add missing wavoipToken column in MySQL schema 2025-02-14 15:26:07 -03:00
Davidson Gomes
fc513f1d1d
Merge pull request #1240 from ygorsantana/fix/list-response-message
fix: change mediaId optional chaining and list response message text obtain
2025-02-14 13:05:34 -03:00
Ygor Santana
6f1667abb5 style: run lint 2025-02-14 12:58:46 -03:00
Davidson Gomes
98b8419b8d
Merge pull request #1237 from GrimBit1/main
Refactor Editing Message events and update message handler
2025-02-14 12:09:27 -03:00
Ygor Santana
59479f9a21 fix: obtain mediaUrl not defined 2025-02-13 22:35:09 -03:00
Ygor Santana
ff77bc018a fix: change mediaId optional chaining and list response message text obtain 2025-02-13 21:54:09 -03:00
Aditya Nandwana
33f7f2932d Implement message update handling in BaileysStartupService 2025-02-13 11:07:18 +05:30
Aditya Nandwana
c939ed2337 Enhance message editing validation in BaileysStartupService 2025-02-13 10:39:29 +05:30
Davidson Gomes
a49c97996c
Merge pull request #1235 from julianoaj/fix-1234-migration-wavoipToken-create
🐛 Fix: Add migration for missing wavoipToken column in MySQL build env (#1234)
2025-02-12 18:23:35 -03:00
julianoaj
da72edfb03 🐛 Corrige problema na API relacionado à migration. Fixes #1234
Corrige problema na API relacionado à migration. Fixes #1234
2025-02-12 16:14:21 -03:00
Davidson Gomes
b51526aff3
Merge pull request #1226 from Desarrollo-TMS/fix-csat-chatwoot
fix: chatwoot csat creating new conversation in another language
2025-02-11 12:14:50 -03:00
Alexis Hernandez
b93ee2e023 fix: chatwoot csat creating new conversation in another language 2025-02-11 08:20:36 -04:00
Davidson Gomes
b58d9e957f Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2025-02-10 12:53:06 -03:00
Davidson Gomes
e5137e1aac
Merge pull request #1221 from mbap-dev/fix-audio
Fix audio send duplicate from chatwoot.
2025-02-10 12:52:48 -03:00
mbap-dev
6b120e5da2 Fix audio send duplicate from chatwoot. 2025-02-09 20:12:40 -03:00
Davidson Gomes
342dacc398
Merge pull request #1217 from Desarrollo-TMS/feat-message-location
feat: add message location support whatsapp meta
2025-02-07 16:57:45 -03:00
Alexis Hernandez
d75c37e233 feat: add message location support meta 2025-02-07 13:04:36 -04:00
Davidson Gomes
36df38d78b
Merge pull request #1215 from rafwell/main
Fix instance creation on v2.2.3
2025-02-07 12:34:32 -03:00
Davidson Gomes
68c6ad4f91
Merge pull request #1211 from joaosouz4dev/main
feat: notconvertsticket for animated stickers
2025-02-07 11:38:00 -03:00
Davidson Gomes
8685f2fdc4
Merge pull request #1214 from wayre/develop
Adicionado suporte para obter Catálogos e Coleções no WhatsApp Business
2025-02-07 11:19:11 -03:00
Rafael Souza
5a50381a8e Fix table name 2025-02-07 10:07:38 -03:00
Rafael Souza
736ca5e4b6 Fix case in table name 2025-02-07 09:52:08 -03:00
Wayre Avelar
6c1355b64b feat: Criado um novo grupo de rotas (business) para tratar dos catalogos de produtos e Coleções evitando alterações desnecessárias em arquivos do repositório 2025-02-07 00:56:49 -03:00
João Victor Souza
95401cf9b0
fix: rollback deploy_database.sh 2025-02-06 18:05:31 -03:00
João Victor Souza
0c5d28bb6c Merge branch 'main' of https://github.com/EvolutionAPI/evolution-api 2025-02-06 18:02:36 -03:00
João Victor Souza
0d1e7c08c9 feat: sendsticket notconvertsticket 2025-02-06 17:56:21 -03:00
Davidson Gomes
d665474404 feat: Add NATS integration support to the event system
- Added NATS package to dependencies
- Created Prisma schema models for NATS configuration
- Implemented NATS controller, router, and event management
- Updated instance controller and event manager to support NATS
- Added NATS configuration options in environment configuration
- Included NATS events in instance validation schema
2025-02-05 17:05:29 -03:00
Davidson Gomes
9a72b90ab2 refactor: Make RabbitMQ prefix key optional in configuration 2025-02-04 17:57:21 -03:00
Davidson Gomes
b143363c5d
Merge pull request #1201 from wayre/catalogProducts
Feat: Adicionei suporte para obter o Catálogos de Produtos e as Coleções de Produtos para a versão 2.2.3
2025-02-04 09:59:59 -03:00
Wayre Avelar
56a8e09ba8 chore: eslint applied 2025-02-04 09:04:41 -03:00
Davidson Gomes
023e030802
Merge pull request #1195 from GrimBit1/main
Refactor edit and delete message functionality in BaileyStartupService
2025-02-04 06:22:49 -03:00
Wayre Avelar
e27db0612f feat: Add support to get Catalogs and Collections with new routes: '{{baseUrl}}/chat/fetchCatalogs' and '{{baseUrl}}/chat/fetchCollections' 2025-02-04 03:51:47 -03:00
Davidson Gomes
e51b6e9270 fix: improve message deduplication and edited message handling in Baileys service
- Refactor edited message detection logic
- Prevent duplicate message processing for edited messages
- Optimize message key caching mechanism
2025-02-03 17:19:07 -03:00
Davidson Gomes
867e8493aa fix: added cache to identify duplicated messages on events
- Update Docker image repository to evoapicloud/evolution-api
- Modify contact email to contato@evolution-api.com
- Update Docker Compose, Dockerfile, and workflow files
- Add Docker image badge to README
- Include additional content creator in README
- Implement message deduplication cache in Baileys service
2025-02-03 15:37:11 -03:00
Davidson Gomes
8135994340 Merge tag '2.2.3' into develop
v
2025-02-03 11:52:51 -03:00
Aditya Nandwana
91e7a32209 Refactor createJid method in BaileysStartupService 2025-02-03 14:24:27 +05:30
Aditya Nandwana
4137984b5d Refactor message deletion in BaileysStartupService 2025-02-03 11:44:41 +05:30
Aditya Nandwana
96821f5d9a Refactor BaileysStartupService updateMessage method 2025-02-03 11:43:53 +05:30
João Victor Souza
2fb6318011 feat: updates api evolution for docker 2024-12-27 10:40:04 -03:00
107 changed files with 9406 additions and 10007 deletions

View File

@ -1,5 +1,7 @@
.git
*Dockerfile*
*docker-compose*
package-lock.json
.env
node_modules
dist

View File

@ -3,6 +3,9 @@ SERVER_PORT=8080
# Server URL - Set your application url
SERVER_URL=http://localhost:8080
SSL_CONF_PRIVKEY=/path/to/cert.key
SSL_CONF_FULLCHAIN=/path/to/cert.crt
SENTRY_DSN=
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
@ -47,6 +50,7 @@ DATABASE_DELETE_MESSAGE=true
RABBITMQ_ENABLED=false
RABBITMQ_URI=amqp://localhost
RABBITMQ_EXCHANGE_NAME=evolution
RABBITMQ_FRAME_MAX=8192
# Global events - By enabling this variable, events from all instances are sent in the same event queue.
RABBITMQ_GLOBAL_ENABLED=false
# Prefix key to queue name
@ -62,6 +66,7 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
RABBITMQ_EVENTS_MESSAGES_UPDATE=false
RABBITMQ_EVENTS_MESSAGES_DELETE=false
RABBITMQ_EVENTS_SEND_MESSAGE=false
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
RABBITMQ_EVENTS_CONTACTS_SET=false
RABBITMQ_EVENTS_CONTACTS_UPSERT=false
RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -108,6 +113,7 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
PUSHER_EVENTS_MESSAGES_UPDATE=true
PUSHER_EVENTS_MESSAGES_DELETE=true
PUSHER_EVENTS_SEND_MESSAGE=true
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
PUSHER_EVENTS_CONTACTS_SET=true
PUSHER_EVENTS_CONTACTS_UPSERT=true
PUSHER_EVENTS_CONTACTS_UPDATE=true
@ -149,6 +155,7 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
WEBHOOK_EVENTS_MESSAGES_DELETE=true
WEBHOOK_EVENTS_SEND_MESSAGE=true
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
WEBHOOK_EVENTS_CONTACTS_SET=true
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
@ -173,6 +180,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
WEBHOOK_EVENTS_ERRORS=false
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
WEBHOOK_REQUEST_TIMEOUT_MS=60000
WEBHOOK_RETRY_MAX_ATTEMPTS=10
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
WEBHOOK_RETRY_JITTER_FACTOR=0.2
# Comma separated list of HTTP status codes that should not trigger retries
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
# Name that will be displayed on smartphone connection
CONFIG_SESSION_PHONE_CLIENT=Evolution API
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
@ -180,7 +196,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web
CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
# Set qrcode display limit
QRCODE_LIMIT=30
@ -210,6 +226,12 @@ OPENAI_ENABLED=false
# Dify - Environment variables
DIFY_ENABLED=false
# n8n - Environment variables
N8N_ENABLED=false
# EvoAI - Environment variables
EVOAI_ENABLED=false
# Cache - Environment variables
# Redis Cache enabled
CACHE_REDIS_ENABLED=true
@ -266,4 +288,4 @@ LANGUAGE=en
# PROXY_PORT=80
# PROXY_PROTOCOL=http
# PROXY_USERNAME=
# PROXY_PASSWORD=
# PROXY_PASSWORD=

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: type=semver,pattern=v{{version}}
- name: Set up QEMU

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: homolog
- name: Set up QEMU

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: latest
- name: Set up QEMU

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
/dist
/node_modules
.cursor*
/Docker/.env
.vscode

View File

@ -1,3 +1,34 @@
# 2.3.0 (2025-06-17 09:19)
### Feature
* Add support to get Catalogs and Collections with new routes: '{{baseUrl}}/chat/fetchCatalogs' and '{{baseUrl}}/chat/fetchCollections'
* Add NATS integration support to the event system
* Add message location support meta
* Add S3_SKIP_POLICY env variable to disable setBucketPolicy for incompatible providers
* Add EvoAI integration with models, services, and routes
* Add N8n integration with models, services, and routes
### Fixed
* Shell injection vulnerability
* Update Baileys Version v6.7.18
* Audio send duplicate from chatwoot
* Chatwoot csat creating new conversation in another language
* Refactor SQS controller to correct bug in sqs events by instance
* Adjustin cloud api send audio and video
* Preserve animation in GIF and WebP stickers
* Preventing use conversation from other inbox for the same user
* Ensure full WhatsApp compatibility for audio conversion (libopus, 48kHz, mono)
* Enhance message fetching and processing logic
* Added lid on whatsapp numbers router
* Now if the CONFIG_SESSION_PHONE_VERSION variable is not filled in it automatically searches for the most updated version
### Security
* Change execSync to execFileSync
* Enhance WebSocket authentication and connection handling
# 2.2.3 (2025-02-03 11:52)
### Fixed

View File

@ -2,7 +2,7 @@ version: "3.7"
services:
evolution_v2:
image: atendai/evolution-api:v2.1.2
image: atendai/evolution-api:v2.2.3
volumes:
- evolution_instances:/evolution/instances
networks:
@ -34,6 +34,7 @@ services:
- RABBITMQ_EVENTS_MESSAGES_UPDATE=false
- RABBITMQ_EVENTS_MESSAGES_DELETE=false
- RABBITMQ_EVENTS_SEND_MESSAGE=false
- RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
- RABBITMQ_EVENTS_CONTACTS_SET=false
- RABBITMQ_EVENTS_CONTACTS_UPSERT=false
- RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -71,6 +72,7 @@ services:
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
- WEBHOOK_EVENTS_MESSAGES_DELETE=true
- WEBHOOK_EVENTS_SEND_MESSAGE=true
- WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
- WEBHOOK_EVENTS_CONTACTS_SET=true
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true
@ -92,7 +94,7 @@ services:
- WEBHOOK_EVENTS_ERRORS_WEBHOOK=
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
- CONFIG_SESSION_PHONE_NAME=Chrome
- CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
- QRCODE_LIMIT=30
- OPENAI_ENABLED=true
- DIFY_ENABLED=true

View File

@ -1,11 +1,11 @@
FROM node:20-alpine AS builder
RUN apk update && \
apk add git ffmpeg wget curl bash openssl
apk add --no-cache git ffmpeg wget curl bash openssl
LABEL version="2.2.3" description="Api to control whatsapp features through http requests."
LABEL version="2.3.0" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@atendai.com"
LABEL contact="contato@evolution-api.com"
WORKDIR /evolution

View File

@ -8,7 +8,7 @@ a. LOGO and copyright information: In the process of using Evolution API's front
b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
Please contact contato@atendai.com to inquire about licensing matters.
Please contact contato@evolution-api.com to inquire about licensing matters.
2. As a contributor, you should agree that:

View File

@ -2,6 +2,7 @@
<div align="center">
[![Docker Image (https://img.shields.io/badge/Docker-Image-blue)](https://hub.docker.com/r/evoapicloud/evolution-api)]
[![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](https://evolution-api.com/whatsapp)
[![Discord Community](https://img.shields.io/badge/Discord-Community-blue)](https://evolution-api.com/discord)
[![Postman Collection](https://img.shields.io/badge/Postman-Collection-orange)](https://evolution-api.com/postman)
@ -87,6 +88,7 @@ https://github.com/sponsors/EvolutionAPI
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
- [Promovaweb](https://www.youtube.com/@promovaweb)
- [Sandeco](https://www.youtube.com/@canalsandeco)
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
- [Francis MNO](https://www.youtube.com/@FrancisMNO)
- [Pablo Cabral](https://youtube.com/@pablocabral)
@ -111,7 +113,7 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
Please contact contato@atendai.com to inquire about licensing matters.
Please contact contato@evolution-api.com to inquire about licensing matters.
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).

View File

@ -1,7 +1,7 @@
services:
api:
container_name: evolution_api
image: atendai/evolution-api:homolog
image: evoapicloud/evolution-api:latest
restart: always
depends_on:
- redis

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/assets/images/evolution-logo.png" />
<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-CFAZX6IV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
</head>
<body>
<div id="root"></div>

3273
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.2.3",
"version": "2.3.0",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js",
"type": "commonjs",
@ -41,7 +41,7 @@
],
"author": {
"name": "Davidson Gomes",
"email": "contato@atendai.com"
"email": "contato@evolution-api.com"
},
"license": "Apache-2.0",
"bugs": {
@ -58,6 +58,7 @@
"@prisma/client": "^6.1.0",
"@sentry/node": "^8.47.0",
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1",
@ -75,6 +76,7 @@
"jimp": "^0.16.13",
"json-schema": "^0.4.0",
"jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"link-preview-js": "^3.0.13",
"long": "^5.2.3",
"mediainfo.js": "^0.3.4",
@ -82,6 +84,7 @@
"mime-types": "^2.1.35",
"minio": "^8.0.3",
"multer": "^1.4.5-lts.1",
"nats": "^2.29.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",
"openai": "^4.77.3",

View File

@ -0,0 +1,9 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Setting`
ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100);

View File

@ -0,0 +1,175 @@
/*
Warnings:
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- A unique constraint covering the columns `[instanceId,remoteJid]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE `Setting` ADD COLUMN `wavoipToken` VARCHAR(100) NULL,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX `Chat_instanceId_remoteJid_key` ON `Chat`(`instanceId`, `remoteJid`);

View File

@ -0,0 +1,26 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
SET @column_exists := (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'Setting'
AND column_name = 'wavoipToken'
);
SET @sql := IF(@column_exists = 0,
'ALTER TABLE Setting ADD COLUMN wavoipToken VARCHAR(100);',
'SELECT "Column already exists";'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE Chat ADD CONSTRAINT unique_remote_instance UNIQUE (remoteJid, instanceId);

View File

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "mysql"

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy?
Setting Setting?
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Websocket Websocket?
Typebot Typebot[]
@ -99,12 +100,13 @@ model Instance {
Template Template[]
Dify Dify[]
DifySetting DifySetting?
integrationSessions IntegrationSession[]
IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
Pusher Pusher?
N8n N8n[]
}
model Session {
@ -116,18 +118,19 @@ model Session {
}
model Chat {
id String @id @default(cuid())
remoteJid String @db.VarChar(100)
name String? @db.VarChar(100)
labels Json? @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
id String @id @default(cuid())
remoteJid String @db.VarChar(100)
name String? @db.VarChar(100)
labels Json? @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
unreadMessages Int @default(0)
@@unique([instanceId, remoteJid])
@@index([instanceId])
@@index([remoteJid])
@@unique([instanceId, remoteJid])
}
model Contact {
@ -170,6 +173,7 @@ model Message {
sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId])
}
@ -185,6 +189,7 @@ model MessageUpdate {
messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@@index([instanceId])
@@index([messageId])
}
@ -201,6 +206,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -269,6 +275,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -282,6 +289,16 @@ model Rabbitmq {
instanceId String @unique
}
model Nats {
id String @id @default(cuid())
enabled Boolean @default(false)
events Json @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Sqs {
id String @id @default(cuid())
enabled Boolean @default(false)
@ -382,7 +399,7 @@ model IntegrationSession {
model Media {
id String @id @default(cuid())
fileName String @unique @db.VarChar(500)
fileName String @db.VarChar(500)
type String @db.VarChar(100)
mimetype String @db.VarChar(100)
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
@ -627,3 +644,100 @@ model IsOnWhatsapp {
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
}
model N8n {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
webhookUrl String? @db.VarChar(255)
basicAuthUser String? @db.VarChar(255)
basicAuthPass String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
N8nSetting N8nSetting[]
}
model N8nSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
n8nIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Evoai {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
EvoaiSetting EvoaiSetting[]
}
model EvoaiSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
evoaiIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}

View File

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Nats" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"events" JSONB NOT NULL,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "Nats_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Nats_instanceId_key" ON "Nats"("instanceId");
-- AddForeignKey
ALTER TABLE "Nats" ADD CONSTRAINT "Nats_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,62 @@
-- CreateTable
CREATE TABLE "N8n" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"description" VARCHAR(255),
"webhookUrl" VARCHAR(255),
"basicAuthUser" VARCHAR(255),
"basicAuthPass" VARCHAR(255),
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"splitMessages" BOOLEAN DEFAULT false,
"timePerChar" INTEGER DEFAULT 50,
"triggerType" "TriggerType",
"triggerOperator" "TriggerOperator",
"triggerValue" TEXT,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "N8n_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "N8nSetting" (
"id" TEXT NOT NULL,
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"splitMessages" BOOLEAN DEFAULT false,
"timePerChar" INTEGER DEFAULT 50,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"n8nIdFallback" VARCHAR(100),
"instanceId" TEXT NOT NULL,
CONSTRAINT "N8nSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "N8nSetting_instanceId_key" ON "N8nSetting"("instanceId");
-- AddForeignKey
ALTER TABLE "N8n" ADD CONSTRAINT "N8n_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_n8nIdFallback_fkey" FOREIGN KEY ("n8nIdFallback") REFERENCES "N8n"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,61 @@
-- CreateTable
CREATE TABLE "Evoai" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"description" VARCHAR(255),
"agentUrl" VARCHAR(255),
"apiKey" VARCHAR(255),
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"splitMessages" BOOLEAN DEFAULT false,
"timePerChar" INTEGER DEFAULT 50,
"triggerType" "TriggerType",
"triggerOperator" "TriggerOperator",
"triggerValue" TEXT,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "Evoai_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EvoaiSetting" (
"id" TEXT NOT NULL,
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"splitMessages" BOOLEAN DEFAULT false,
"timePerChar" INTEGER DEFAULT 50,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"evoaiIdFallback" VARCHAR(100),
"instanceId" TEXT NOT NULL,
CONSTRAINT "EvoaiSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "EvoaiSetting_instanceId_key" ON "EvoaiSetting"("instanceId");
-- AddForeignKey
ALTER TABLE "Evoai" ADD CONSTRAINT "Evoai_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_evoaiIdFallback_fkey" FOREIGN KEY ("evoaiIdFallback") REFERENCES "Evoai"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "Media_fileName_key";

View File

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "Typebot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false,
ADD COLUMN "timePerChar" INTEGER DEFAULT 50;
-- AlterTable
ALTER TABLE "TypebotSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false,
ADD COLUMN "timePerChar" INTEGER DEFAULT 50;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "IsOnWhatsapp" ADD COLUMN "lid" VARCHAR(100);

View File

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy?
Setting Setting?
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Websocket Websocket?
Typebot Typebot[]
@ -99,12 +100,16 @@ model Instance {
Template Template[]
Dify Dify[]
DifySetting DifySetting?
integrationSessions IntegrationSession[]
IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
Pusher Pusher?
N8n N8n[]
N8nSetting N8nSetting[]
Evoai Evoai[]
EvoaiSetting EvoaiSetting?
}
model Session {
@ -125,6 +130,7 @@ model Chat {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
unreadMessages Int @default(0)
@@index([instanceId])
@@index([remoteJid])
}
@ -168,6 +174,7 @@ model Message {
sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId])
}
@ -183,6 +190,7 @@ model MessageUpdate {
messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@@index([instanceId])
@@index([messageId])
}
@ -199,6 +207,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -269,6 +278,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -282,6 +292,16 @@ model Rabbitmq {
instanceId String @unique
}
model Nats {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean
events Json @db.JsonB
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Sqs {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean
@ -337,6 +357,8 @@ model Typebot {
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
TypebotSetting TypebotSetting[]
@ -354,6 +376,8 @@ model TypebotSetting {
debounceTime Int? @db.Integer
typebotIdFallback String? @db.VarChar(100)
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
@ -363,7 +387,7 @@ model TypebotSetting {
model Media {
id String @id @default(cuid())
fileName String @unique @db.VarChar(500)
fileName String @db.VarChar(500)
type String @db.VarChar(100)
mimetype String @db.VarChar(100)
createdAt DateTime? @default(now()) @db.Date
@ -624,6 +648,104 @@ model IsOnWhatsapp {
id String @id @default(cuid())
remoteJid String @unique @db.VarChar(100)
jidOptions String
lid String? @db.VarChar(100)
createdAt DateTime @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
}
model N8n {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
webhookUrl String? @db.VarChar(255)
basicAuthUser String? @db.VarChar(255)
basicAuthPass String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
N8nSetting N8nSetting[]
}
model N8nSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
n8nIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Evoai {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
EvoaiSetting EvoaiSetting[]
}
model EvoaiSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
evoaiIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}

View File

@ -0,0 +1,15 @@
import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BusinessController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
}
public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
}
}

View File

@ -170,6 +170,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},
@ -258,6 +261,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},

View File

@ -18,6 +18,14 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { BadRequestException } from '@exceptions';
import { isBase64, isURL } from 'class-validator';
function isEmoji(str: string) {
if (str === '') return true;
const emojiRegex =
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
return emojiRegex.test(str);
}
export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {}
@ -81,8 +89,8 @@ export class SendMessageController {
}
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
throw new BadRequestException('"reaction" must be an emoji');
if (!isEmoji(data.reaction)) {
throw new BadRequestException('Reaction must be a single emoji or empty string');
}
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
}

View File

@ -0,0 +1,14 @@
export class NumberDto {
number: string;
}
export class getCatalogDto {
number?: string;
limit?: number;
cursor?: string;
}
export class getCollectionsDto {
number?: string;
limit?: number;
}

View File

@ -13,6 +13,7 @@ export class OnWhatsAppDto {
public readonly exists: boolean,
public readonly number: string,
public readonly name?: string,
public readonly lid?: string,
) {}
}

View File

@ -44,6 +44,7 @@ export class Metadata {
mentionsEveryOne?: boolean;
mentioned?: string[];
encoding?: boolean;
notConvertSticker?: boolean;
}
export class SendTextDto extends Metadata {

View File

@ -165,11 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService {
openAiDefaultSettings.speechToText &&
received?.message?.audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
received,
this.client.updateMediaMessage,
);
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
}
}

View File

@ -28,7 +28,6 @@ import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import mimeTypes from 'mime-types';
import { join } from 'path';
@ -147,11 +146,20 @@ export class BusinessStartupService extends ChannelStartupService {
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
// Primeiro, obtenha a URL do arquivo
let result = await axios.get(urlServer, { headers });
result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
// Depois, baixe o arquivo usando a URL retornada
result = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
return result.data;
} catch (e) {
this.logger.error(e);
this.logger.error(`Error downloading media: ${e}`);
throw e;
}
}
@ -159,7 +167,23 @@ export class BusinessStartupService extends ChannelStartupService {
const message = received.messages[0];
let content: any = message.type + 'Message';
content = { [content]: message[message.type] };
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
private messageAudioJson(received: any) {
const message = received.messages[0];
let content: any = {
audioMessage: {
...message.audio,
ptt: message.audio.voice || false, // Define se é mensagem de voz
},
};
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
@ -192,17 +216,77 @@ export class BusinessStartupService extends ChannelStartupService {
}
private messageTextJson(received: any) {
let content: any;
// Verificar que received y received.messages existen
if (!received || !received.messages || received.messages.length === 0) {
this.logger.error('Error: received object or messages array is undefined or empty');
return null;
}
const message = received.messages[0];
let content: any;
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
if (!message.text) {
// Si no hay texto, manejamos diferente según el tipo de mensaje
if (message.type === 'sticker') {
content = { stickerMessage: {} };
} else if (message.type === 'location') {
content = {
locationMessage: {
degreesLatitude: message.location?.latitude,
degreesLongitude: message.location?.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
} else {
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
content = { [message.type + 'Message']: message[message.type] || {} };
}
// Añadir contexto si existe
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
// Si el mensaje tiene texto, procesamos normalmente
if (!received.metadata || !received.metadata.phone_number_id) {
this.logger.error('Error: metadata or phone_number_id is undefined');
return null;
}
if (message.from === received.metadata.phone_number_id) {
content = {
extendedTextMessage: { text: message.text.body },
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
} else {
content = { conversation: message.text.body };
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
}
return content;
}
private messageLocationJson(received: any) {
const message = received.messages[0];
let content: any = {
locationMessage: {
degreesLatitude: message.location.latitude,
degreesLongitude: message.location.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
return content;
}
@ -283,6 +367,12 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template':
messageType = 'conversation';
break;
case 'location':
messageType = 'locationMessage';
break;
case 'sticker':
messageType = 'stickerMessage';
break;
default:
messageType = 'conversation';
break;
@ -299,17 +389,36 @@ export class BusinessStartupService extends ChannelStartupService {
if (received.contacts) pushName = received.contacts[0].profile.name;
if (received.messages) {
const message = received.messages[0]; // Añadir esta línea para definir message
const key = {
id: received.messages[0].id,
id: message.id,
remoteJid: this.phoneNumber,
fromMe: received.messages[0].from === received.metadata.phone_number_id,
fromMe: message.from === received.metadata.phone_number_id,
};
if (this.isMediaMessage(received?.messages[0])) {
if (message.type === 'sticker') {
this.logger.log('Procesando mensaje de tipo sticker');
messageRaw = {
key,
pushName,
message: this.messageMediaJson(received),
contextInfo: this.messageMediaJson(received)?.contextInfo,
message: {
stickerMessage: message.sticker || {},
},
messageType: 'stickerMessage',
messageTimestamp: parseInt(message.timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (this.isMediaMessage(message)) {
const messageContent =
message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
messageRaw = {
key,
pushName,
message: messageContent,
contextInfo: messageContent?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown',
@ -327,7 +436,10 @@ export class BusinessStartupService extends ChannelStartupService {
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers });
const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
const buffer = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
let mediaType;
@ -352,6 +464,17 @@ export class BusinessStartupService extends ChannelStartupService {
}
}
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
}
}
const size = result.headers['content-length'] || buffer.data.byteLength;
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
@ -378,13 +501,72 @@ export class BusinessStartupService extends ChannelStartupService {
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
} else {
const buffer = await this.downloadMediaMessage(received?.messages[0]);
messageRaw.message.base64 = buffer.toString('base64');
// Processar OpenAI speech-to-text para áudio mesmo sem S3
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
base64: messageRaw.message.base64,
...messageRaw,
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
}
} else if (received?.messages[0].interactive) {
messageRaw = {
@ -455,37 +637,6 @@ export class BusinessStartupService extends ChannelStartupService {
// await this.client.readMessages([received.key]);
}
if (this.configService.get<Openai>('OPENAI').ENABLED) {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
const audioMessage = received?.messages[0]?.audio;
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText &&
audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
() => {},
);
}
}
this.logger.log(messageRaw);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
@ -511,7 +662,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
}
if (!this.isMediaMessage(received?.messages[0])) {
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
await this.prismaRepository.message.create({
data: messageRaw,
});
@ -714,17 +865,54 @@ export class BusinessStartupService extends ChannelStartupService {
}
protected async eventHandler(content: any) {
const database = this.configService.get<Database>('DATABASE');
const settings = await this.findSettings();
try {
// Registro para depuración
this.logger.log('Contenido recibido en eventHandler:');
this.logger.log(JSON.stringify(content, null, 2));
this.messageHandle(content, database, settings);
const database = this.configService.get<Database>('DATABASE');
const settings = await this.findSettings();
// Si hay mensajes, verificar primero el tipo
if (content.messages && content.messages.length > 0) {
const message = content.messages[0];
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
// Verificamos el tipo de mensaje antes de procesarlo
if (
message.type === 'text' ||
message.type === 'image' ||
message.type === 'video' ||
message.type === 'audio' ||
message.type === 'document' ||
message.type === 'sticker' ||
message.type === 'location' ||
message.type === 'contacts' ||
message.type === 'interactive' ||
message.type === 'button' ||
message.type === 'reaction'
) {
// Procesar el mensaje normalmente
this.messageHandle(content, database, settings);
} else {
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
}
} else if (content.statuses) {
// Procesar actualizaciones de estado
this.messageHandle(content, database, settings);
} else {
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
}
} catch (error) {
this.logger.error('Error en eventHandler:');
this.logger.error(error);
}
}
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
try {
let quoted: any;
let webhookUrl: any;
const linkPreview = options?.linkPreview != false ? undefined : false;
if (options?.quoted) {
const m = options?.quoted;
@ -792,7 +980,7 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
text: {
body: message['conversation'],
preview_url: linkPreview,
preview_url: Boolean(options?.linkPreview),
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;
@ -808,9 +996,10 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
caption: message['caption'],
...(message['mediaType'] !== 'audio' &&
message['fileName'] &&
!isImage && { filename: message['fileName'] }),
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;
@ -908,7 +1097,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
})();
if (messageSent?.error_data) {
if (messageSent?.error_data || messageSent.message) {
this.logger.error(messageSent);
return messageSent;
}
@ -975,29 +1164,50 @@ export class BusinessStartupService extends ChannelStartupService {
return res;
}
private async getIdMedia(mediaMessage: any) {
const formData = new FormData();
private async getIdMedia(mediaMessage: any, isFile = false) {
try {
const formData = new FormData();
const fileStream = createReadStream(mediaMessage.media);
if (isFile === false) {
if (isURL(mediaMessage.media)) {
const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
const buffer = Buffer.from(response.data, 'base64');
formData.append('file', buffer, {
filename: mediaMessage.fileName || 'media',
contentType: mediaMessage.mimetype,
});
} else {
const buffer = Buffer.from(mediaMessage.media, 'base64');
formData.append('file', buffer, {
filename: mediaMessage.fileName || 'media',
contentType: mediaMessage.mimetype,
});
}
} else {
formData.append('file', mediaMessage.media.buffer, {
filename: mediaMessage.media.originalname,
contentType: mediaMessage.media.mimetype,
});
}
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype);
formData.append('messaging_product', 'whatsapp');
const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
// const fileBuffer = await fs.readFile(mediaMessage.media);
formData.append('typeFile', mimetype);
formData.append('messaging_product', 'whatsapp');
// const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype });
// formData.append('file', fileBlob);
// formData.append('typeFile', mediaMessage.mimetype);
// formData.append('messaging_product', 'whatsapp');
const token = this.token;
const headers = { Authorization: `Bearer ${this.token}` };
const res = await axios.post(
process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media',
formData,
{ headers },
);
return res.data.id;
const headers = { Authorization: `Bearer ${token}` };
const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
}/${this.number}/media`;
const res = await axios.post(url, formData, { headers });
return res.data.id;
} catch (error) {
this.logger.error(error.response.data);
throw new InternalServerErrorException(error?.toString() || error);
}
}
protected async prepareMediaMessage(mediaMessage: MediaMessage) {
@ -1070,48 +1280,87 @@ export class BusinessStartupService extends ChannelStartupService {
return mediaSent;
}
public async processAudio(audio: string, number: string) {
public async processAudio(audio: string, number: string, file: any) {
number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`;
let mimetype: string | false;
if (process.env.API_AUDIO_CONVERTER) {
this.logger.verbose('Using audio converter API');
const formData = new FormData();
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
};
if (file) {
formData.append('file', file.buffer, {
filename: file.originalname,
contentType: file.mimetype,
});
} else if (isURL(audio)) {
formData.append('url', audio);
} else {
formData.append('base64', audio);
}
formData.append('format', 'mp3');
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
headers: {
...formData.getHeaders(),
apikey: process.env.API_AUDIO_CONVERTER_KEY,
},
});
const audioConverter = response?.data?.audio || response?.data?.url;
if (!audioConverter) {
throw new InternalServerErrorException('Failed to convert audio');
}
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audioConverter,
mimetype: 'audio/mpeg',
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else {
mimetype = mimeTypes.lookup(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
this.logger.verbose('Audio converted');
return prepareMedia;
} else {
let mimetype: string | false;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else if (audio && !file) {
mimetype = mimeTypes.lookup(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
} else if (file) {
prepareMedia.media = file;
const id = await this.getIdMedia(prepareMedia, true);
prepareMedia.id = id;
prepareMedia.type = 'id';
mimetype = file.mimetype;
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data };
if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else if (isURL(mediaData.audio)) {
// DO NOTHING
// mediaData.audio = mediaData.audio;
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
}
const message = await this.processAudio(mediaData.audio, data.number);
const message = await this.processAudio(data.audio, data.number, file);
const audioSent = await this.sendMessageWithTyping(
data.number,

View File

@ -0,0 +1,935 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { TriggerOperator, TriggerType } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { BaseChatbotDto } from './base-chatbot.dto';
import { ChatbotController, ChatbotControllerInterface, EmitData } from './chatbot.controller';
// Common settings interface for all chatbot integrations
export interface ChatbotSettings {
expire: number;
keywordFinish: string;
delayMessage: number;
unknownMessage: string;
listeningFromMe: boolean;
stopBotFromMe: boolean;
keepOpen: boolean;
debounceTime: number;
ignoreJids: string[];
splitMessages: boolean;
timePerChar: number;
[key: string]: any;
}
// Common bot properties for all chatbot integrations
export interface BaseBotData {
enabled?: boolean;
description: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: string | TriggerType;
triggerOperator?: string | TriggerOperator;
triggerValue?: string;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
[key: string]: any;
}
export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto>
extends ChatbotController
implements ChatbotControllerInterface
{
public readonly logger: Logger;
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Name of the integration, to be set by the derived class
protected abstract readonly integrationName: string;
// Method to process bot-specific logic
protected abstract processBot(
waInstance: any,
remoteJid: string,
bot: BotType,
session: any,
settings: ChatbotSettings,
content: string,
pushName?: string,
msg?: any,
): Promise<void>;
// Method to get the fallback bot ID from settings
protected abstract getFallbackBotId(settings: any): string | undefined;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor);
this.sessionRepository = this.prismaRepository.integrationSession;
}
// Base create bot implementation
public async createBot(instance: InstanceDto, data: BotData) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// Set default settings if not provided
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids ||
!data.splitMessages ||
!data.timePerChar
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck?.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck?.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck?.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck?.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck?.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck?.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck?.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck?.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error(
`You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
);
}
// Check for trigger keyword duplicates
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
// Check for trigger advanced duplicates
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
// Derived classes should implement the specific duplicate checking before calling this method
// and add bot-specific fields to the data object
try {
const botData = {
enabled: data?.enabled,
description: data.description,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
...this.getAdditionalBotData(data),
};
const bot = await this.botRepository.create({
data: botData,
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error creating ${this.integrationName}`);
}
}
// Additional fields needed for specific bot types
protected abstract getAdditionalBotData(data: BotData): Record<string, any>;
// Common implementation for findBot
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
try {
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
return bots;
} catch (error) {
this.logger.error(error);
throw new Error(`Error finding ${this.integrationName}`);
}
}
// Common implementation for fetchBot
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const bot = await this.botRepository.findUnique({
where: {
id: botId,
},
});
if (!bot) {
return null;
}
return bot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error fetching ${this.integrationName}`);
}
}
// Common implementation for settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const existingSettings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
// Get the name of the fallback field for this integration type
const fallbackFieldName = this.getFallbackFieldName();
const settingsData = {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
[fallbackFieldName]: data.fallbackId, // Use the correct field name dynamically
};
if (existingSettings) {
const settings = await this.settingsRepository.update({
where: {
id: existingSettings.id,
},
data: settingsData,
});
// Map the specific fallback field to a generic 'fallbackId' in the response
return {
...settings,
fallbackId: settings[fallbackFieldName],
};
} else {
const settings = await this.settingsRepository.create({
data: {
...settingsData,
Instance: {
connect: {
id: instanceId,
},
},
},
});
// Map the specific fallback field to a generic 'fallbackId' in the response
return {
...settings,
fallbackId: settings[fallbackFieldName],
};
}
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Abstract method to get the field name for the fallback ID
protected abstract getFallbackFieldName(): string;
// Abstract method to get the integration type (dify, n8n, evoai, etc.)
protected abstract getIntegrationType(): string;
// Common implementation for fetchSettings
public async fetchSettings(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
// Get the name of the fallback field for this integration type
const fallbackFieldName = this.getFallbackFieldName();
if (!settings) {
return {
expire: 300,
keywordFinish: 'bye',
delayMessage: 1000,
unknownMessage: 'Sorry, I dont understand',
listeningFromMe: true,
stopBotFromMe: true,
keepOpen: false,
debounceTime: 1,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
fallbackId: '',
fallback: null,
};
}
// Return with standardized fallbackId field
return {
...settings,
fallbackId: settings[fallbackFieldName],
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching settings');
}
}
// Common implementation for changeStatus
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error(`Error changing ${this.integrationName} status`);
}
}
// Common implementation for fetchSessions
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error(`${this.integrationName} not found`);
}
// Get the integration type (dify, n8n, evoai, etc.)
const integrationType = this.getIntegrationType();
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: integrationType,
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
// Common implementation for ignoreJid
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Base implementation for updateBot
public async updateBot(instance: InstanceDto, botId: string, data: BotData) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error(`${this.integrationName} not found`);
}
if (bot.instanceId !== instanceId) {
throw new Error(`${this.integrationName} not found`);
}
// Check for "all" trigger type conflicts
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error(
`You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
);
}
}
// Let subclasses check for integration-specific duplicates
await this.validateNoDuplicatesOnUpdate(botId, instanceId, data);
// Check for keyword trigger duplicates
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
// Check for advanced trigger duplicates
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
// Combine common fields with bot-specific fields
const updateData = {
enabled: data?.enabled,
description: data.description,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
...this.getAdditionalUpdateFields(data),
};
const updatedBot = await this.botRepository.update({
where: {
id: botId,
},
data: updateData,
});
return updatedBot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error updating ${this.integrationName}`);
}
}
// Abstract method for validating bot-specific duplicates on update
protected abstract validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: BotData): Promise<void>;
// Abstract method for getting additional fields for update
protected abstract getAdditionalUpdateFields(data: BotData): Record<string, any>;
// Base implementation for deleteBot
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error(`${this.integrationName} not found`);
}
if (bot.instanceId !== instanceId) {
throw new Error(`${this.integrationName} not found`);
}
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error(`Error deleting ${this.integrationName} bot`);
}
}
// Base implementation for emit
public async emit({ instance, remoteJid, msg }: EmitData) {
if (!this.integrationEnabled) return;
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
// Get integration type
// const integrationType = this.getIntegrationType();
// Find a bot for this message
let findBot: any = await this.findBotTrigger(this.botRepository, content, instance, session);
// If no bot is found, try to use fallback
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
// Get the fallback ID for this integration type
const fallbackId = this.getFallbackBotId(fallback);
if (fallbackId) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallbackId,
},
});
findBot = findFallback;
} else {
return;
}
}
// If we still don't have a bot, return
if (!findBot) {
return;
}
// Collect settings with fallbacks to default settings
let expire = findBot.expire;
let keywordFinish = findBot.keywordFinish;
let delayMessage = findBot.delayMessage;
let unknownMessage = findBot.unknownMessage;
let listeningFromMe = findBot.listeningFromMe;
let stopBotFromMe = findBot.stopBotFromMe;
let keepOpen = findBot.keepOpen;
let debounceTime = findBot.debounceTime;
let ignoreJids = findBot.ignoreJids;
let splitMessages = findBot.splitMessages;
let timePerChar = findBot.timePerChar;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
// Handle stopping the bot if message is from me
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
// Skip if not listening to messages from me
if (!listeningFromMe && key.fromMe) {
return;
}
// Skip if session exists but not awaiting user input
if (session && session.status === 'closed') {
return;
}
// Skip if session exists and status is paused
if (session && session.status === 'paused') {
this.logger.warn(`Session for ${remoteJid} is paused, skipping message processing`);
return;
}
// Merged settings
const mergedSettings = {
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
};
// Process with debounce if needed
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
mergedSettings,
debouncedContent,
msg?.pushName,
msg,
);
});
} else {
await this.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
mergedSettings,
content,
msg?.pushName,
msg,
);
}
} catch (error) {
this.logger.error(error);
}
}
}

View File

@ -0,0 +1,42 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
/**
* Base DTO for all chatbot integrations
* Contains common properties shared by all chatbot types
*/
export class BaseChatbotDto {
enabled?: boolean;
description: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
}
/**
* Base settings DTO for all chatbot integrations
*/
export class BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
fallbackId?: string; // Unified fallback ID field for all integrations
}

View File

@ -0,0 +1,412 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';
/**
* Base class for all chatbot service implementations
* Contains common methods shared across different chatbot integrations
*/
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
protected readonly logger: Logger;
protected readonly waMonitor: WAMonitoringService;
protected readonly prismaRepository: PrismaRepository;
protected readonly configService?: ConfigService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
loggerName: string,
configService?: ConfigService,
) {
this.waMonitor = waMonitor;
this.prismaRepository = prismaRepository;
this.logger = new Logger(loggerName);
this.configService = configService;
}
/**
* Check if a message contains an image
*/
protected isImageMessage(content: string): boolean {
return content.includes('imageMessage');
}
/**
* Check if a message contains audio
*/
protected isAudioMessage(content: string): boolean {
return content.includes('audioMessage');
}
/**
* Check if a string is valid JSON
*/
protected isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
/**
* Determine the media type from a URL based on its extension
*/
protected getMediaType(url: string): string | null {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
}
/**
* Create a new chatbot session
*/
public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
try {
// Extract pushName safely - if data.pushName is an object with a pushName property, use that
const pushNameValue =
typeof data.pushName === 'object' && data.pushName?.pushName
? data.pushName.pushName
: typeof data.pushName === 'string'
? data.pushName
: null;
// Extract remoteJid safely
const remoteJidValue =
typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: remoteJidValue,
pushName: pushNameValue,
sessionId: remoteJidValue,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: type,
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
/**
* Standard implementation for processing incoming messages
* This handles the common workflow across all chatbot types:
* 1. Check for existing session or create new one
* 2. Handle message based on session state
*/
public async process(
instance: any,
remoteJid: string,
bot: BotType,
session: IntegrationSession,
settings: SettingsType,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
try {
// For new sessions or sessions awaiting initialization
if (!session) {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
return;
}
// If session is paused, ignore the message
if (session.status === 'paused') {
return;
}
// For existing sessions, keywords might indicate the conversation should end
const keywordFinish = (settings as any)?.keywordFinish || '';
const normalizedContent = content.toLowerCase().trim();
if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
// Update session to closed and return
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
return;
}
// Forward the message to the chatbot API
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
// Update session to indicate we're waiting for user response
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
} catch (error) {
this.logger.error(`Error in process: ${error}`);
return;
}
}
/**
* Standard implementation for sending messages to WhatsApp
* This handles common patterns like markdown links and formatting
*/
protected async sendMessageWhatsApp(
instance: any,
remoteJid: string,
message: string,
settings: SettingsType,
): Promise<void> {
if (!message) return;
const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const splitMessages = (settings as any)?.splitMessages ?? false;
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
// Send accumulated text before sending media
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
textBuffer = '';
}
// Handle sending the media
try {
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
audio: url,
caption: altText,
});
} else {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
mediatype: mediaType,
media: url,
caption: altText,
fileName: mediaType === 'document' ? altText || 'document' : undefined,
},
null,
false,
);
}
} catch (error) {
this.logger.error(`Error sending media: ${error}`);
// If media fails, at least send the alt text and URL
textBuffer += `${altText}: ${url}`;
}
} else {
// It's a regular link, keep it in the text
textBuffer += fullMatch;
}
lastIndex = linkRegex.lastIndex;
}
// Add any remaining text after the last match
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
// Send any remaining text
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
}
}
/**
* Helper method to send formatted text with proper typing indicators and delays
*/
private async sendFormattedText(
instance: any,
remoteJid: string,
text: string,
settings: any,
splitMessages: boolean,
): Promise<void> {
const timePerChar = settings?.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const multipleMessages = text.split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
if (!message.trim()) continue;
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: text,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
/**
* Standard implementation for initializing a new session
* This method should be overridden if a subclass needs specific initialization
*/
protected async initNewSession(
instance: any,
remoteJid: string,
bot: BotType,
settings: SettingsType,
session: IntegrationSession,
content: string,
pushName?: string | any,
msg?: any,
): Promise<void> {
// Create a session if none exists
if (!session) {
// Extract pushName properly - if it's an object with pushName property, use that
const pushNameValue =
typeof pushName === 'object' && pushName?.pushName
? pushName.pushName
: typeof pushName === 'string'
? pushName
: null;
const sessionResult = await this.createNewSession(
{
instanceName: instance.instanceName,
instanceId: instance.instanceId,
},
{
remoteJid,
pushName: pushNameValue,
botId: (bot as any).id,
},
this.getBotType(),
);
if (!sessionResult || !sessionResult.session) {
this.logger.error('Failed to create new session');
return;
}
session = sessionResult.session;
}
// Update session status to opened
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
// Forward the message to the chatbot
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
}
/**
* Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
* This should match the type field used in the IntegrationSession
*/
protected abstract getBotType(): string;
/**
* Send a message to the chatbot API
* This is specific to each chatbot integration
*/
protected abstract sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: SettingsType,
bot: BotType,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void>;
}

View File

@ -2,8 +2,10 @@ import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import {
difyController,
evoaiController,
evolutionBotController,
flowiseController,
n8nController,
openaiController,
typebotController,
} from '@api/server.module';
@ -97,6 +99,10 @@ export class ChatbotController {
await difyController.emit(emitData);
await n8nController.emit(emitData);
await evoaiController.emit(emitData);
await flowiseController.emit(emitData);
}
@ -173,7 +179,7 @@ export class ChatbotController {
if (session) {
if (session.status !== 'closed' && !session.botId) {
this.logger.warn('Session is already opened in another integration');
return;
return null;
} else if (!session.botId) {
session = null;
}
@ -188,13 +194,13 @@ export class ChatbotController {
instance: InstanceDto,
session?: IntegrationSession,
) {
let findBot: null;
let findBot: any = null;
if (!session) {
findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
if (!findBot) {
return;
return null;
}
} else {
findBot = await botRepository.findFirst({

View File

@ -4,8 +4,10 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
import { Router } from 'express';
import { EvoaiRouter } from './evoai/routes/evoai.router';
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
import { FlowiseRouter } from './flowise/routes/flowise.router';
import { N8nRouter } from './n8n/routes/n8n.router';
export class ChatbotRouter {
public readonly router: Router;
@ -19,5 +21,7 @@ export class ChatbotRouter {
this.router.use('/openai', new OpenaiRouter(...guards).router);
this.router.use('/dify', new DifyRouter(...guards).router);
this.router.use('/flowise', new FlowiseRouter(...guards).router);
this.router.use('/n8n', new N8nRouter(...guards).router);
this.router.use('/evoai', new EvoaiRouter(...guards).router);
}
}

View File

@ -1,6 +1,8 @@
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema';
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';

View File

@ -295,51 +295,57 @@ export class ChatwootService {
avatar_url?: string,
jid?: string,
) {
const client = await this.clientCw(instance);
try {
const client = await this.clientCw(instance);
if (!client) {
this.logger.warn('client not found');
return null;
}
let data: any = {};
if (!isGroup) {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: jid,
avatar_url: avatar_url,
};
if ((jid && jid.includes('@')) || !jid) {
data['phone_number'] = `+${phoneNumber}`;
if (!client) {
this.logger.warn('client not found');
return null;
}
} else {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: phoneNumber,
avatar_url: avatar_url,
};
}
const contact = await client.contacts.create({
accountId: this.provider.accountId,
data,
});
let data: any = {};
if (!isGroup) {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: jid,
avatar_url: avatar_url,
};
if (!contact) {
this.logger.warn('contact not found');
if ((jid && jid.includes('@')) || !jid) {
data['phone_number'] = `+${phoneNumber}`;
}
} else {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: phoneNumber,
avatar_url: avatar_url,
};
}
const contact = await client.contacts.create({
accountId: this.provider.accountId,
data,
});
if (!contact) {
this.logger.warn('contact not found');
return null;
}
const findContact = await this.findContact(instance, phoneNumber);
const contactId = findContact?.id;
await this.addLabelToContact(this.provider.nameInbox, contactId);
return contact;
} catch (error) {
this.logger.error('Error creating contact');
console.log(error);
return null;
}
const findContact = await this.findContact(instance, phoneNumber);
const contactId = findContact?.id;
await this.addLabelToContact(this.provider.nameInbox, contactId);
return contact;
}
public async updateContact(instance: InstanceDto, id: number, data: any) {
@ -543,216 +549,240 @@ export class ChatwootService {
}
public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds
try {
this.logger.verbose('--- Start createConversation ---');
// Processa atualização de contatos já criados @lid
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`,
);
await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`,
});
}
}
this.logger.verbose(`--- Start createConversation ---`);
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
const client = await this.clientCw(instance);
if (!client) {
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
return null;
}
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
this.logger.verbose(`Cache key: ${cacheKey}`);
// If it already exists in the cache, return conversationId
if (await this.cache.has(cacheKey)) {
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Cached 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);
}
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
return conversationId;
}
const isGroup = body.key.remoteJid.includes('@g.us');
this.logger.verbose(`Is group: ${isGroup}`);
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
this.logger.verbose(`Chat ID: ${chatId}`);
let nameContact: string;
nameContact = !body.key.fromMe ? body.pushName : chatId;
this.logger.verbose(`Name contact: ${nameContact}`);
const filterInbox = await this.getInbox(instance);
if (!filterInbox) {
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
return null;
// If lock already exists, wait until release or timeout
if (await this.cache.has(lockKey)) {
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
const start = Date.now();
while (await this.cache.has(lockKey)) {
if (Date.now() - start > maxWaitTime) {
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
break;
}
await new Promise((res) => setTimeout(res, 300));
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
return conversationId;
}
}
}
if (isGroup) {
this.logger.verbose('Processing group conversation');
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
// Adquire lock
await this.cache.set(lockKey, true, 30);
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
nameContact = `${group.subject} (GROUP)`;
try {
/*
Double check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
body.key.participant.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const client = await this.clientCw(instance);
if (!client) return null;
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
const isGroup = remoteJid.includes('@g.us');
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
let nameContact = !body.key.fromMe ? body.pushName : chatId;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
if (findParticipant) {
if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
name: body.pushName,
avatar_url: picture_url.profilePictureUrl || null,
});
if (isGroup) {
this.logger.verbose(`Processing group conversation`);
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
nameContact = `${group.subject} (GROUP)`;
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
body.key.participant.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
if (findParticipant) {
if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
name: body.pushName,
avatar_url: picture_url.profilePictureUrl || null,
});
}
} else {
await this.createContact(
instance,
body.key.participant.split('@')[0],
filterInbox.id,
isGroup,
body.pushName,
picture_url.profilePictureUrl || null,
body.key.participant,
);
}
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
let contact = await this.findContact(instance, chatId);
if (contact) {
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
if (!body.key.fromMe) {
const waProfilePictureFile =
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
if (pictureNeedsUpdate || nameNeedsUpdate) {
contact = await this.updateContact(instance, contact.id, {
...(nameNeedsUpdate && { name: nameContact }),
...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
});
}
}
} else {
await this.createContact(
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact(
instance,
body.key.participant.split('@')[0],
chatId,
filterInbox.id,
false,
body.pushName,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
body.key.participant,
jid,
);
}
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
let contact = await this.findContact(instance, chatId);
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
if (contact) {
if (!body.key.fromMe) {
const waProfilePictureFile =
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
if (pictureNeedsUpdate || nameNeedsUpdate) {
contact = await this.updateContact(instance, contact.id, {
...(nameNeedsUpdate && { name: nameContact }),
...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
});
}
if (!contact) {
this.logger.warn(`Contact not created or found`);
return null;
}
} else {
const jid = body.key.remoteJid;
contact = await this.createContact(
instance,
chatId,
filterInbox.id,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
this.logger.verbose(`Contact ID: ${contactId}`);
const contactConversations = (await client.contacts.listConversations({
accountId: this.provider.accountId,
id: contactId,
})) as any;
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
if (!contactConversations || !contactConversations.payload) {
this.logger.error(`No conversations found or payload is undefined`);
return null;
}
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
);
}
if (!contact) {
this.logger.warn('Contact not created or found');
return null;
}
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
this.logger.verbose(`Contact ID: ${contactId}`);
const contactConversations = (await client.contacts.listConversations({
accountId: this.provider.accountId,
id: contactId,
})) as any;
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
if (!contactConversations || !contactConversations.payload) {
this.logger.error('No conversations found or payload is undefined');
return null;
}
if (contactConversations.payload.length) {
let conversation: any;
if (this.provider.reopenConversation) {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
if (this.provider.conversationPending && conversation.status !== 'open') {
if (conversation) {
if (inboxConversation) {
if (this.provider.reopenConversation) {
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: conversation.id,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
}
} else {
inboxConversation = contactConversations.payload.find(
(conversation) =>
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
} else {
conversation = contactConversations.payload.find(
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
}
if (conversation) {
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
const data = {
contact_id: contactId.toString(),
inbox_id: filterInbox.id.toString(),
};
if (this.provider.conversationPending) {
data['status'] = 'pending';
}
/*
Triple check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn(`Conversation not created or found`);
return null;
}
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} finally {
await this.cache.delete(lockKey);
this.logger.verbose(`Block released for: ${lockKey}`);
}
const data = {
contact_id: contactId.toString(),
inbox_id: filterInbox.id.toString(),
};
if (this.provider.conversationPending) {
data['status'] = 'pending';
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn('Conversation not created or found');
return null;
}
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} catch (error) {
this.logger.error(`Error in createConversation: ${error}`);
return null;
}
}
@ -931,7 +961,7 @@ export class ChatwootService {
quotedMsg?: MessageModel,
) {
if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
if (messageAlreadySaved) {
if (messageAlreadySaved.size > 0) {
this.logger.warn('Message already saved on chatwoot');
@ -1106,12 +1136,13 @@ export class ChatwootService {
sendTelemetry('/message/sendWhatsAppAudio');
const messageSent = await waInstance?.audioWhatsapp(data, true);
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
return messageSent;
}
if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
type = 'document';
}
@ -1653,7 +1684,7 @@ export class ChatwootService {
stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
@ -1898,7 +1929,7 @@ export class ChatwootService {
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage;
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
return;
}
@ -2199,7 +2230,7 @@ export class ChatwootService {
}
}
if (event === 'messages.edit') {
if (event === 'messages.edit' || event === 'send.message.update') {
const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;

View File

@ -169,7 +169,7 @@ class ChatwootImport {
}
}
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise<Set<string>> {
try {
const existingSourceIdsSet = new Set<string>();
@ -177,18 +177,25 @@ class ChatwootImport {
return existingSourceIdsSet;
}
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
// Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
const pgClient = postgresClient.getChatwootConnection();
const result = await pgClient.query(query, [formattedSourceIds]);
const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds];
const query = conversationId
? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'
: 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
const result = await pgClient.query(query, params);
for (const row of result.rows) {
existingSourceIdsSet.add(row.source_id);
}
return existingSourceIdsSet;
} catch (error) {
return null;
this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`);
return new Set<string>();
}
}
@ -499,25 +506,30 @@ class ChatwootImport {
stickerMessage: msg.message.stickerMessage,
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
};
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
switch (typeKey) {
case 'documentMessage':
return `_<File: ${msg.message.documentMessage.fileName}${
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
}>_`;
case 'documentMessage': {
const doc = msg.message.documentMessage;
const fileName = doc?.fileName || 'document';
const caption = doc?.caption ? ` ${doc.caption}` : '';
return `_<File: ${fileName}${caption}>_`;
}
case 'documentWithCaptionMessage':
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
msg.message.documentWithCaptionMessage.message.documentMessage.caption
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
: ''
}>_`;
case 'documentWithCaptionMessage': {
const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
const fileName = doc?.fileName || 'document';
const caption = doc?.caption ? ` ${doc.caption}` : '';
return `_<File: ${fileName}${caption}>_`;
}
case 'templateMessage':
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
case 'templateMessage': {
const template = msg.message.templateMessage?.hydratedTemplate;
return (
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
(template?.hydratedContentText || '')
);
}
case 'imageMessage':
return '_<Image Message>_';

View File

@ -1,4 +1,3 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
@ -7,12 +6,11 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Dify } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { Dify as DifyModel } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class DifyController extends ChatbotController implements ChatbotControllerInterface {
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
constructor(
private readonly difyService: DifyService,
prismaRepository: PrismaRepository,
@ -26,6 +24,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
}
public readonly logger = new Logger('DifyController');
protected readonly integrationName = 'Dify';
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
botRepository: any;
@ -33,261 +32,37 @@ export class DifyController extends ChatbotController implements ChatbotControll
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids ||
!data.splitMessages ||
!data.timePerChar
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data?.enabled,
description: data.description,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating dify');
}
protected getFallbackBotId(settings: any): string | undefined {
return settings?.fallbackId;
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
protected getFallbackFieldName(): string {
return 'difyIdFallback';
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return bot;
protected getIntegrationType(): string {
return 'dify';
}
public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
protected getAdditionalBotData(data: DifyDto): Record<string, any> {
return {
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
return {
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
@ -303,81 +78,10 @@ export class DifyController extends ChatbotController implements ChatbotControll
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data?.enabled,
description: data.description,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating dify');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
// Override createBot to add Dify-specific validation
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
@ -388,501 +92,35 @@ export class DifyController extends ChatbotController implements ChatbotControll
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
// Dify-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: botId,
instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (!bot) {
throw new Error('Dify not found');
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting dify bot');
}
// Let the base class handle the rest
return super.createBot(instance, data);
}
// Settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
difyIdFallback: updateSettings.difyIdFallback,
ignoreJids: updateSettings.ignoreJids,
splitMessages: updateSettings.splitMessages,
timePerChar: updateSettings.timePerChar,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
difyIdFallback: newSetttings.difyIdFallback,
ignoreJids: newSetttings.ignoreJids,
splitMessages: newSetttings.splitMessages,
timePerChar: newSetttings.timePerChar,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async fetchSettings(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
difyIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
splitMessages: settings.splitMessages,
timePerChar: settings.timePerChar,
difyIdFallback: settings.difyIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: 'dify',
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Emit
public async emit({ instance, remoteJid, msg }: EmitData) {
if (!this.integrationEnabled) return;
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.difyIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.difyIdFallback,
},
});
findBot = findFallback;
} else {
return;
}
}
let expire = findBot?.expire;
let keywordFinish = findBot?.keywordFinish;
let delayMessage = findBot?.delayMessage;
let unknownMessage = findBot?.unknownMessage;
let listeningFromMe = findBot?.listeningFromMe;
let stopBotFromMe = findBot?.stopBotFromMe;
let keepOpen = findBot?.keepOpen;
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.difyService.processDify(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
debouncedContent,
msg?.pushName,
);
});
} else {
await this.difyService.processDify(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
// Process Dify-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: DifyModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,38 +1,13 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
import { $Enums } from '@prisma/client';
export class DifyDto {
enabled?: boolean;
description?: string;
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class DifyDto extends BaseChatbotDto {
botType?: $Enums.DifyBotType;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class DifySettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
export class DifySettingDto extends BaseChatbotSettingDto {
difyIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -1,60 +1,34 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { ConfigService, HttpServer } from '@config/env.config';
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { Readable } from 'stream';
export class DifyService {
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
private openaiService: OpenaiService;
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
private readonly logger = new Logger('DifyService');
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
pushName: data.pushName,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: 'dify',
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'DifyService', configService);
this.openaiService = openaiService;
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
/**
* Return the bot type for Dify
*/
protected getBotType(): string {
return 'dify';
}
private isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
private async sendMessageToBot(
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: DifySetting,
@ -62,10 +36,30 @@ export class DifyService {
remoteJid: string,
pushName: string,
content: string,
) {
msg?: any,
): Promise<void> {
try {
let endpoint: string = dify.apiUrl;
if (!endpoint) {
this.logger.error('No Dify endpoint defined');
return;
}
// Handle audio messages - transcribe using OpenAI Whisper
let processedContent = content;
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
processedContent = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
}
}
if (dify.botType === 'chatBot') {
endpoint += '/chat-messages';
const payload: any = {
@ -74,17 +68,17 @@ export class DifyService {
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
apiKey: instance.token,
},
query: content,
query: processedContent,
response_mode: 'blocking',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -112,7 +106,9 @@ export class DifyService {
const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -130,21 +126,21 @@ export class DifyService {
endpoint += '/completion-messages';
const payload: any = {
inputs: {
query: content,
query: processedContent,
pushName: pushName,
remoteJid: remoteJid,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
apiKey: instance.token,
},
response_mode: 'blocking',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -172,7 +168,9 @@ export class DifyService {
const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -194,17 +192,17 @@ export class DifyService {
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
apiKey: instance.token,
},
query: content,
query: processedContent,
response_mode: 'streaming',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
@ -230,7 +228,6 @@ export class DifyService {
let answer = '';
const data = response.data.replaceAll('data: ', '');
const events = data.split('\n').filter((line) => line.trim() !== '');
for (const eventString of events) {
@ -248,9 +245,9 @@ export class DifyService {
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = answer;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
if (answer) {
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
}
await this.prismaRepository.integrationSession.update({
where: {
@ -259,369 +256,13 @@ export class DifyService {
data: {
status: 'opened',
awaitUser: true,
sessionId: conversationId,
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
},
});
return;
}
if (dify.botType === 'workflow') {
endpoint += '/workflows/run';
const payload: any = {
inputs: {
query: content,
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
},
response_mode: 'blocking',
user: remoteJid,
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: contentSplit[1].split('?')[0],
},
];
payload.inputs.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
const response = await axios.post(endpoint, payload, {
headers: {
Authorization: `Bearer ${dify.apiKey}`,
},
});
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = response?.data?.data.outputs.text;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
return;
}
} catch (error) {
this.logger.error(error.response?.data || error);
return;
}
}
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const getMediaType = (url: string): string | null => {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
};
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclMark, altText, url] = match;
const mediaType = getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
});
} else {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: mediaType,
media: url,
caption: altText,
},
null,
false,
);
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = linkRegex.lastIndex;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
}
sendTelemetry('/message/sendText');
}
private async initNewSession(
instance: any,
remoteJid: string,
dify: Dify,
settings: DifySetting,
session: IntegrationSession,
content: string,
pushName?: string,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: dify.id,
});
if (data.session) {
session = data.session;
}
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
return;
}
public async processDify(
instance: any,
remoteJid: string,
dify: Dify,
session: IntegrationSession,
settings: DifySetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
return;
}
if (session && settings.expire && settings.expire > 0) {
const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > settings.expire) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: dify.id,
remoteJid: remoteJid,
},
});
}
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
return;
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
if (!content) {
if (settings.unknownMessage) {
this.waMonitor.waInstances[instance.instanceName].textMessage(
{
number: remoteJid.split('@')[0],
delay: settings.delayMessage || 1000,
text: settings.unknownMessage,
},
false,
);
sendTelemetry('/message/sendText');
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: dify.id,
remoteJid: remoteJid,
},
});
}
return;
}
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
return;
}
}

View File

@ -0,0 +1,122 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto';
import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Evoai } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto> {
constructor(
private readonly evoaiService: EvoaiService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.botRepository = this.prismaRepository.evoai;
this.settingsRepository = this.prismaRepository.evoaiSetting;
this.sessionRepository = this.prismaRepository.integrationSession;
}
public readonly logger = new Logger('EvoaiController');
protected readonly integrationName = 'Evoai';
integrationEnabled = configService.get<Evoai>('EVOAI').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
protected getFallbackBotId(settings: any): string | undefined {
return settings?.evoaiIdFallback;
}
protected getFallbackFieldName(): string {
return 'evoaiIdFallback';
}
protected getIntegrationType(): string {
return 'evoai';
}
protected getAdditionalBotData(data: EvoaiDto): Record<string, any> {
return {
agentUrl: data.agentUrl,
apiKey: data.apiKey,
};
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: EvoaiDto): Record<string, any> {
return {
agentUrl: data.agentUrl,
apiKey: data.apiKey,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: EvoaiDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
agentUrl: data.agentUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Evoai already exists');
}
}
// Override createBot to add EvoAI-specific validation
public async createBot(instance: InstanceDto, data: EvoaiDto) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// EvoAI-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
agentUrl: data.agentUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Evoai already exists');
}
// Let the base class handle the rest
return super.createBot(instance, data);
}
// Process Evoai-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: EvoaiModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -0,0 +1,10 @@
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvoaiDto extends BaseChatbotDto {
agentUrl?: string;
apiKey?: string;
}
export class EvoaiSettingDto extends BaseChatbotSettingDto {
evoaiIdFallback?: string;
}

View File

@ -0,0 +1,124 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { evoaiController } from '@api/server.module';
import {
evoaiIgnoreJidSchema,
evoaiSchema,
evoaiSettingSchema,
evoaiStatusSchema,
instanceSchema,
} from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { EvoaiDto, EvoaiSettingDto } from '../dto/evoai.dto';
export class EvoaiRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<EvoaiDto>({
request: req,
schema: evoaiSchema,
ClassRef: EvoaiDto,
execute: (instance, data) => evoaiController.createBot(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => evoaiController.findBot(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:evoaiId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => evoaiController.fetchBot(instance, req.params.evoaiId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:evoaiId'), ...guards, async (req, res) => {
const response = await this.dataValidate<EvoaiDto>({
request: req,
schema: evoaiSchema,
ClassRef: EvoaiDto,
execute: (instance, data) => evoaiController.updateBot(instance, req.params.evoaiId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:evoaiId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => evoaiController.deleteBot(instance, req.params.evoaiId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<EvoaiSettingDto>({
request: req,
schema: evoaiSettingSchema,
ClassRef: EvoaiSettingDto,
execute: (instance, data) => evoaiController.settings(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => evoaiController.fetchSettings(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: evoaiStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => evoaiController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:evoaiId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => evoaiController.fetchSessions(instance, req.params.evoaiId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<IgnoreJidDto>({
request: req,
schema: evoaiIgnoreJidSchema,
ClassRef: IgnoreJidDto,
execute: (instance, data) => evoaiController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -0,0 +1,186 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService, HttpServer } from '@config/env.config';
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
import { v4 as uuidv4 } from 'uuid';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
private openaiService: OpenaiService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'EvoaiService', configService);
this.openaiService = openaiService;
}
/**
* Return the bot type for EvoAI
*/
protected getBotType(): string {
return 'evoai';
}
/**
* Implement the abstract method to send message to EvoAI API
* Handles audio transcription, image processing, and complex JSON-RPC payload
*/
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: EvoaiSetting,
evoai: Evoai,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
try {
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
let processedContent = content;
// Handle audio messages - transcribe using OpenAI Whisper
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
processedContent = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
}
}
const endpoint: string = evoai.agentUrl;
if (!endpoint) {
this.logger.error('No EvoAI endpoint defined');
return;
}
const callId = `req-${uuidv4().substring(0, 8)}`;
const messageId = msg?.key?.id || uuidv4();
// Prepare message parts
const parts = [
{
type: 'text',
text: processedContent,
},
];
// Handle image message if present
if (this.isImageMessage(content) && msg) {
const contentSplit = content.split('|');
parts[0].text = contentSplit[2] || content;
try {
// Download the image
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
const fileContent = Buffer.from(mediaBuffer).toString('base64');
const fileName = contentSplit[2] || `${msg.key?.id || 'image'}.jpg`;
parts.push({
type: 'file',
file: {
name: fileName,
mimeType: 'image/jpeg',
bytes: fileContent,
},
} as any);
} catch (fileErr) {
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
}
}
const payload = {
jsonrpc: '2.0',
id: callId,
method: 'message/send',
params: {
contextId: session.sessionId,
message: {
role: 'user',
parts,
messageId: messageId,
metadata: {
messageKey: msg?.key,
},
},
metadata: {
remoteJid: remoteJid,
pushName: pushName,
fromMe: msg?.key?.fromMe,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
},
};
this.logger.debug(`[EvoAI] Sending request to: ${endpoint}`);
// Redact base64 file bytes from payload log
const redactedPayload = JSON.parse(JSON.stringify(payload));
if (redactedPayload?.params?.message?.parts) {
redactedPayload.params.message.parts = redactedPayload.params.message.parts.map((part) => {
if (part.type === 'file' && part.file && part.file.bytes) {
return { ...part, file: { ...part.file, bytes: '[base64 omitted]' } };
}
return part;
});
}
this.logger.debug(`[EvoAI] Payload: ${JSON.stringify(redactedPayload)}`);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
const response = await axios.post(endpoint, payload, {
headers: {
'x-api-key': evoai.apiKey,
'Content-Type': 'application/json',
},
});
this.logger.debug(`[EvoAI] Response: ${JSON.stringify(response.data)}`);
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
let message = undefined;
const result = response?.data?.result;
// Extract message from artifacts array
if (result?.artifacts && Array.isArray(result.artifacts) && result.artifacts.length > 0) {
const artifact = result.artifacts[0];
if (artifact?.parts && Array.isArray(artifact.parts)) {
const textPart = artifact.parts.find((p) => p.type === 'text' && p.text);
if (textPart) message = textPart.text;
}
}
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
if (message) {
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
} catch (error) {
this.logger.error(
`[EvoAI] Error sending message: ${error?.response?.data ? JSON.stringify(error.response.data) : error}`,
);
return;
}
}
}

View File

@ -0,0 +1,115 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const evoaiSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
agentUrl: { type: 'string' },
apiKey: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
triggerValue: { type: 'string' },
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: ['enabled', 'agentUrl', 'triggerType'],
...isNotEmpty('enabled', 'agentUrl', 'triggerType'),
};
export const evoaiStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
},
required: ['remoteJid', 'status'],
...isNotEmpty('remoteJid', 'status'),
};
export const evoaiSettingSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
),
};
export const evoaiIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -1,16 +1,13 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { EvolutionBot } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { EvolutionBot, IntegrationSession } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
import { EvolutionBotDto } from '../dto/evolutionBot.dto';
import { EvolutionBotService } from '../services/evolutionBot.service';
export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface {
export class EvolutionBotController extends BaseChatbotController<EvolutionBot, EvolutionBotDto> {
constructor(
private readonly evolutionBotService: EvolutionBotService,
prismaRepository: PrismaRepository,
@ -24,258 +21,49 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
}
public readonly logger = new Logger('EvolutionBotController');
protected readonly integrationName = 'EvolutionBot';
integrationEnabled: boolean;
integrationEnabled = true; // Set to true by default or use config value if available
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: EvolutionBotDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// Implementation of abstract methods required by BaseChatbotController
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids ||
!data.splitMessages ||
!data.timePerChar
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data?.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating bot');
}
protected getFallbackBotId(settings: any): string | undefined {
return settings?.botIdFallback;
}
public async findBot(instance: InstanceDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
protected getFallbackFieldName(): string {
return 'botIdFallback';
}
public async fetchBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
return bot;
protected getIntegrationType(): string {
return 'evolution';
}
public async updateBot(instance: InstanceDto, botId: string, data: EvolutionBotDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
protected getAdditionalBotData(data: EvolutionBotDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
}
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: EvolutionBotDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(
botId: string,
instanceId: string,
data: EvolutionBotDto,
): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
@ -288,573 +76,21 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
});
if (checkDuplicate) {
throw new Error('Bot already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data?.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating bot');
throw new Error('Evolution Bot already exists');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
botIdFallback: data.botIdFallback,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
botIdFallback: updateSettings.botIdFallback,
ignoreJids: updateSettings.ignoreJids,
splitMessages: updateSettings.splitMessages,
timePerChar: updateSettings.timePerChar,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
botIdFallback: data.botIdFallback,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
instanceId: instanceId,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
botIdFallback: newSetttings.botIdFallback,
ignoreJids: newSetttings.ignoreJids,
splitMessages: newSetttings.splitMessages,
timePerChar: newSetttings.timePerChar,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async fetchSettings(instance: InstanceDto) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
botIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
splitMessages: settings.splitMessages,
timePerChar: settings.timePerChar,
botIdFallback: settings.botIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
public async changeStatus(instance: InstanceDto, data: any) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: 'evolution',
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Emit
public async emit({ instance, remoteJid, msg }: EmitData) {
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvolutionBot;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.botIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.botIdFallback,
},
});
findBot = findFallback;
} else {
return;
}
}
let expire = findBot?.expire;
let keywordFinish = findBot?.keywordFinish;
let delayMessage = findBot?.delayMessage;
let unknownMessage = findBot?.unknownMessage;
let listeningFromMe = findBot?.listeningFromMe;
let stopBotFromMe = findBot?.stopBotFromMe;
let keepOpen = findBot?.keepOpen;
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.evolutionBotService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
debouncedContent,
msg?.pushName,
);
});
} else {
await this.evolutionBotService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
// Process bot-specific logic
protected async processBot(
instance: any,
remoteJid: string,
bot: EvolutionBot,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,37 +1,10 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvolutionBotDto {
enabled?: boolean;
description?: string;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
export class EvolutionBotDto extends BaseChatbotDto {
apiUrl: string;
apiKey: string;
}
export class EvolutionBotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
export class EvolutionBotSettingDto extends BaseChatbotSettingDto {
botIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -1,428 +1,138 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { ConfigService, HttpServer } from '@config/env.config';
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
export class EvolutionBotService {
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class EvolutionBotService extends BaseChatbotService<EvolutionBot, EvolutionBotSetting> {
private openaiService: OpenaiService;
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
private readonly logger = new Logger('EvolutionBotService');
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
pushName: data.pushName,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: 'evolution',
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
this.openaiService = openaiService;
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
/**
* Get the bot type identifier
*/
protected getBotType(): string {
return 'evolution';
}
private async sendMessageToBot(
/**
* Send a message to the Evolution Bot API
*/
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: EvolutionBotSetting,
bot: EvolutionBot,
remoteJid: string,
pushName: string,
content: string,
) {
const payload: any = {
inputs: {
sessionId: session.id,
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
},
query: content,
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
url: contentSplit[1].split('?')[0],
msg?: any,
): Promise<void> {
try {
const payload: any = {
inputs: {
sessionId: session.id,
remoteJid: remoteJid,
pushName: pushName,
fromMe: msg?.key?.fromMe,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
},
];
payload.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
let headers: any = {
'Content-Type': 'application/json',
};
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
query: content,
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
}
const response = await axios.post(bot.apiUrl, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = response?.data?.message;
return message;
}
private async sendMessageWhatsApp(
instance: any,
remoteJid: string,
session: IntegrationSession,
settings: EvolutionBotSetting,
message: string,
) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const getMediaType = (url: string): string | null => {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
};
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclMark, altText, url] = match;
const mediaType = getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.query = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
}
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
});
} else {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: mediaType,
media: url,
caption: altText,
},
null,
false,
);
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = linkRegex.lastIndex;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
payload.files = [
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
type: 'image',
url: contentSplit[1].split('?')[0],
},
false,
);
];
payload.query = contentSplit[2] || content;
}
textBuffer = '';
}
sendTelemetry('/message/sendText');
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
}
const endpoint = bot.apiUrl;
private async initNewSession(
instance: any,
remoteJid: string,
bot: EvolutionBot,
settings: EvolutionBotSetting,
session: IntegrationSession,
content: string,
pushName?: string,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: bot.id,
});
if (data.session) {
session = data.session;
}
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
if (!message) return;
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
public async processBot(
instance: any,
remoteJid: string,
bot: EvolutionBot,
session: IntegrationSession,
settings: EvolutionBotSetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
return;
}
if (session && settings.expire && settings.expire > 0) {
const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > settings.expire) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
}
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
if (!endpoint) {
this.logger.error('No Evolution Bot endpoint defined');
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
return;
}
let headers: any = {
'Content-Type': 'application/json',
};
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
if (!content) {
if (settings.unknownMessage) {
this.waMonitor.waInstances[instance.instanceName].textMessage(
{
number: remoteJid.split('@')[0],
delay: settings.delayMessage || 1000,
text: settings.unknownMessage,
},
false,
);
sendTelemetry('/message/sendText');
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
};
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
const response = await axios.post(endpoint, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
let message = response?.data?.message;
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
const innerContent = message.slice(1, -1);
if (!innerContent.includes("'")) {
message = innerContent;
}
}
if (message) {
// Use the base class method to send the message to WhatsApp
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
// Send telemetry
sendTelemetry('/message/sendText');
} catch (error) {
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
return;
}
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
if (!message) return;
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
}

View File

@ -1,16 +1,16 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Flowise } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Flowise } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { BadRequestException } from '@exceptions';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
import { FlowiseDto } from '../dto/flowise.dto';
import { FlowiseService } from '../services/flowise.service';
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
constructor(
private readonly flowiseService: FlowiseService,
prismaRepository: PrismaRepository,
@ -24,15 +24,73 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
}
public readonly logger = new Logger('FlowiseController');
protected readonly integrationName = 'Flowise';
integrationEnabled: boolean;
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
protected getFallbackBotId(settings: any): string | undefined {
return settings?.flowiseIdFallback;
}
protected getFallbackFieldName(): string {
return 'flowiseIdFallback';
}
protected getIntegrationType(): string {
return 'flowise';
}
protected getAdditionalBotData(data: FlowiseDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: { not: botId },
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Flowise already exists');
}
}
// Process Flowise-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: FlowiseModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// Override createBot to add module availability check and Flowise-specific validation
public async createBot(instance: InstanceDto, data: FlowiseDto) {
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
@ -41,74 +99,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids ||
!data.splitMessages ||
!data.timePerChar
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a Flowise with an "All" trigger, you cannot have more bots while it is active');
}
// Flowise-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -121,740 +112,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
throw new Error('Flowise already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data?.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating bot');
}
}
public async findBot(instance: InstanceDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
return bot;
}
public async updateBot(instance: InstanceDto, botId: string, data: FlowiseDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
}
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Bot already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data?.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating bot');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
flowiseIdFallback: data.flowiseIdFallback,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
flowiseIdFallback: updateSettings.flowiseIdFallback,
ignoreJids: updateSettings.ignoreJids,
splitMessages: updateSettings.splitMessages,
timePerChar: updateSettings.timePerChar,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
flowiseIdFallback: data.flowiseIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
flowiseIdFallback: newSetttings.flowiseIdFallback,
ignoreJids: newSetttings.ignoreJids,
splitMessages: newSetttings.splitMessages,
timePerChar: newSetttings.timePerChar,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async fetchSettings(instance: InstanceDto) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
flowiseIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
splitMessages: settings.splitMessages,
timePerChar: settings.timePerChar,
flowiseIdFallback: settings.flowiseIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
public async changeStatus(instance: InstanceDto, data: any) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: 'flowise',
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Emit
public async emit({ instance, remoteJid, msg }: EmitData) {
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as Flowise;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.flowiseIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.flowiseIdFallback,
},
});
findBot = findFallback;
} else {
return;
}
}
let expire = findBot?.expire;
let keywordFinish = findBot?.keywordFinish;
let delayMessage = findBot?.delayMessage;
let unknownMessage = findBot?.unknownMessage;
let listeningFromMe = findBot?.listeningFromMe;
let stopBotFromMe = findBot?.stopBotFromMe;
let keepOpen = findBot?.keepOpen;
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.flowiseService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
debouncedContent,
msg?.pushName,
);
});
} else {
await this.flowiseService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
// Let the base class handle the rest
return super.createBot(instance, data);
}
}

View File

@ -1,37 +1,10 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class FlowiseDto {
enabled?: boolean;
description?: string;
apiUrl?: string;
export class FlowiseDto extends BaseChatbotDto {
apiUrl: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class FlowiseSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
export class FlowiseSettingDto extends BaseChatbotSettingDto {
flowiseIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -1,50 +1,57 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import { ConfigService, HttpServer } from '@config/env.config';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import axios from 'axios';
export class FlowiseService {
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
private openaiService: OpenaiService;
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
private readonly logger = new Logger('FlowiseService');
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
pushName: data.pushName,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: 'flowise',
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'FlowiseService', configService);
this.openaiService = openaiService;
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
// Return the bot type for Flowise
protected getBotType(): string {
return 'flowise';
}
private async sendMessageToBot(instance: any, bot: Flowise, remoteJid: string, pushName: string, content: string) {
// Process Flowise-specific bot logic
public async processBot(
instance: any,
remoteJid: string,
bot: FlowiseModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// Implement the abstract method to send message to Flowise API
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: any,
bot: FlowiseModel,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
const payload: any = {
question: content,
overrideConfig: {
@ -54,11 +61,24 @@ export class FlowiseService {
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
apiKey: instance.token,
},
},
};
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.question = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
}
}
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
@ -91,335 +111,26 @@ export class FlowiseService {
const endpoint = bot.apiUrl;
if (!endpoint) return null;
if (!endpoint) {
this.logger.error('No Flowise endpoint defined');
return;
}
const response = await axios.post(endpoint, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS)
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
const message = response?.data?.text;
return message;
if (message) {
// Use the base class method to send the message to WhatsApp
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
}
}
private async sendMessageWhatsApp(
instance: any,
remoteJid: string,
session: IntegrationSession,
settings: FlowiseSetting,
message: string,
) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const getMediaType = (url: string): string | null => {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
};
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclMark, altText, url] = match;
const mediaType = getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
});
} else {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: mediaType,
media: url,
caption: altText,
},
null,
false,
);
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = linkRegex.lastIndex;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
textBuffer = '';
}
sendTelemetry('/message/sendText');
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
return;
}
private async initNewSession(
instance: any,
remoteJid: string,
bot: Flowise,
settings: FlowiseSetting,
session: IntegrationSession,
content: string,
pushName?: string,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: bot.id,
});
if (data.session) {
session = data.session;
}
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
public async processBot(
instance: any,
remoteJid: string,
bot: Flowise,
session: IntegrationSession,
settings: FlowiseSetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
return;
}
if (session && settings.expire && settings.expire > 0) {
const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > settings.expire) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
}
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
return;
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
if (!content) {
if (settings.unknownMessage) {
this.waMonitor.waInstances[instance.instanceName].textMessage(
{
number: remoteJid.split('@')[0],
delay: settings.delayMessage || 1000,
text: settings.unknownMessage,
},
false,
);
sendTelemetry('/message/sendText');
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
}
return;
}
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
// The service is now complete with just the abstract method implementations
}

View File

@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
flowiseIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: [
'expire',

View File

@ -0,0 +1,127 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { N8nDto } from '@api/integrations/chatbot/n8n/dto/n8n.dto';
import { N8nService } from '@api/integrations/chatbot/n8n/services/n8n.service';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { IntegrationSession, N8n as N8nModel } from '@prisma/client';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
constructor(
private readonly n8nService: N8nService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.botRepository = this.prismaRepository.n8n;
this.settingsRepository = this.prismaRepository.n8nSetting;
this.sessionRepository = this.prismaRepository.integrationSession;
}
public readonly logger = new Logger('N8nController');
protected readonly integrationName = 'N8n';
integrationEnabled = configService.get('N8N').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
protected getFallbackBotId(settings: any): string | undefined {
return settings?.fallbackId;
}
protected getFallbackFieldName(): string {
return 'n8nIdFallback';
}
protected getIntegrationType(): string {
return 'n8n';
}
protected getAdditionalBotData(data: N8nDto): Record<string, any> {
return {
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
};
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: N8nDto): Record<string, any> {
return {
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: N8nDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
},
});
if (checkDuplicate) {
throw new Error('N8n already exists');
}
}
// Bots
public async createBot(instance: InstanceDto, data: N8nDto) {
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// Check for N8n-specific duplicate
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
},
});
if (checkDuplicate) {
throw new Error('N8n already exists');
}
// Let the base class handle the rest of the bot creation process
return super.createBot(instance, data);
}
// Process N8n-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: N8nModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
// Use the base class pattern instead of calling n8nService.process directly
await this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -0,0 +1,17 @@
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class N8nDto extends BaseChatbotDto {
// N8n specific fields
webhookUrl?: string;
basicAuthUser?: string;
basicAuthPass?: string;
}
export class N8nSettingDto extends BaseChatbotSettingDto {
// N8n has no specific fields
}
export class N8nMessageDto {
chatInput: string;
sessionId: string;
}

View File

@ -0,0 +1,114 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { n8nController } from '@api/server.module';
import {
instanceSchema,
n8nIgnoreJidSchema,
n8nSchema,
n8nSettingSchema,
n8nStatusSchema,
} from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { N8nDto, N8nSettingDto } from '../dto/n8n.dto';
export class N8nRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nDto>({
request: req,
schema: n8nSchema,
ClassRef: N8nDto,
execute: (instance, data) => n8nController.createBot(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.findBot(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.fetchBot(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nDto>({
request: req,
schema: n8nSchema,
ClassRef: N8nDto,
execute: (instance, data) => n8nController.updateBot(instance, req.params.n8nId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.deleteBot(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nSettingDto>({
request: req,
schema: n8nSettingSchema,
ClassRef: N8nSettingDto,
execute: (instance, data) => n8nController.settings(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.fetchSettings(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: n8nStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => n8nController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.fetchSessions(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<IgnoreJidDto>({
request: req,
schema: n8nIgnoreJidSchema,
ClassRef: IgnoreJidDto,
execute: (instance, data) => n8nController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -0,0 +1,96 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { ConfigService, HttpServer } from '@config/env.config';
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
private openaiService: OpenaiService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'N8nService', configService);
this.openaiService = openaiService;
}
/**
* Return the bot type for N8n
*/
protected getBotType(): string {
return 'n8n';
}
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: N8nSetting,
n8n: N8n,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
) {
try {
if (!session) {
this.logger.error('Session is null in sendMessageToBot');
return;
}
const endpoint: string = n8n.webhookUrl;
const payload: any = {
chatInput: content,
sessionId: session.sessionId,
remoteJid: remoteJid,
pushName: pushName,
fromMe: msg?.key?.fromMe,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: instance.token,
};
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.chatInput = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
}
}
const headers: Record<string, string> = {};
if (n8n.basicAuthUser && n8n.basicAuthPass) {
const auth = Buffer.from(`${n8n.basicAuthUser}:${n8n.basicAuthPass}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
}
const response = await axios.post(endpoint, payload, { headers });
const message = response?.data?.output || response?.data?.answer;
// Use base class method instead of custom implementation
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
} catch (error) {
this.logger.error(error.response?.data || error);
return;
}
}
}

View File

@ -0,0 +1,116 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const n8nSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
webhookUrl: { type: 'string' },
basicAuthUser: { type: 'string' },
basicAuthPassword: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
triggerValue: { type: 'string' },
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: ['enabled', 'webhookUrl', 'triggerType'],
...isNotEmpty('enabled', 'webhookUrl', 'triggerType'),
};
export const n8nStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
},
required: ['remoteJid', 'status'],
...isNotEmpty('remoteJid', 'status'),
};
export const n8nSettingSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
'splitMessages',
'timePerChar',
),
};
export const n8nIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -1,15 +1,13 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class OpenaiCredsDto {
name: string;
apiKey: string;
}
export class OpenaiDto {
enabled?: boolean;
description?: string;
export class OpenaiDto extends BaseChatbotDto {
openaiCredsId: string;
botType?: string;
botType: string;
assistantId?: string;
functionUrl?: string;
model?: string;
@ -17,35 +15,10 @@ export class OpenaiDto {
assistantMessages?: string[];
userMessages?: string[];
maxTokens?: number;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class OpenaiSettingDto {
export class OpenaiSettingDto extends BaseChatbotSettingDto {
openaiCredsId?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
openaiIdFallback?: string;
ignoreJids?: any;
speechToText?: boolean;
splitMessages?: boolean;
timePerChar?: number;
}

View File

@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => openaiController.getModels(instance),
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
});
res.status(HttpStatus.OK).json(response);

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
@ -8,13 +7,12 @@ import { Events } from '@api/types/wa.types';
import { configService, Typebot } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { Typebot as TypebotModel } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
import axios from 'axios';
import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class TypebotController extends ChatbotController implements ChatbotControllerInterface {
export class TypebotController extends BaseChatbotController<TypebotModel, TypebotDto> {
constructor(
private readonly typebotService: TypebotService,
prismaRepository: PrismaRepository,
@ -28,6 +26,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
}
public readonly logger = new Logger('TypebotController');
protected readonly integrationName = 'Typebot';
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
botRepository: any;
@ -35,245 +34,35 @@ export class TypebotController extends ChatbotController implements ChatbotContr
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: TypebotDto) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
url: data.url,
typebot: data.typebot,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Typebot already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data?.enabled,
description: data.description,
url: data.url,
typebot: data.typebot,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating typebot');
}
protected getFallbackBotId(settings: any): string | undefined {
return settings?.typebotIdFallback;
}
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
protected getFallbackFieldName(): string {
return 'typebotIdFallback';
}
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Typebot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Typebot not found');
}
return bot;
protected getIntegrationType(): string {
return 'typebot';
}
public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
return {
url: data.url,
typebot: data.typebot,
};
}
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const typebot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!typebot) {
throw new Error('Typebot not found');
}
if (typebot.instanceId !== instanceId) {
throw new Error('Typebot not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error(
'You already have a typebot with an "All" trigger, you cannot have more bots while it is active',
);
}
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
return {
url: data.url,
typebot: data.typebot,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
url: data.url,
@ -288,263 +77,41 @@ export class TypebotController extends ChatbotController implements ChatbotContr
if (checkDuplicate) {
throw new Error('Typebot already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data?.enabled,
description: data.description,
url: data.url,
typebot: data.typebot,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating typebot');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const typebot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!typebot) {
throw new Error('Typebot not found');
}
if (typebot.instanceId !== instanceId) {
throw new Error('Typebot not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { typebot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting typebot');
}
// Process Typebot-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: TypebotModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
// Map to the original processTypebot method signature
await this.typebotService.processTypebot(
instance,
remoteJid,
msg,
session,
bot,
bot.url,
settings.expire,
bot.typebot,
settings.keywordFinish,
settings.delayMessage,
settings.unknownMessage,
settings.listeningFromMe,
settings.stopBotFromMe,
settings.keepOpen,
content,
{}, // prefilledVariables (optional)
);
}
// Settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
typebotIdFallback: data.typebotIdFallback,
ignoreJids: data.ignoreJids,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
typebotIdFallback: updateSettings.typebotIdFallback,
ignoreJids: updateSettings.ignoreJids,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
typebotIdFallback: data.typebotIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
typebotIdFallback: newSetttings.typebotIdFallback,
ignoreJids: newSetttings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async fetchSettings(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
typebotIdFallback: null,
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
typebotIdFallback: settings.typebotIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
// TypeBot specific method for starting a bot from API
public async startBot(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
@ -552,7 +119,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
const instanceData = await this.prismaRepository.instance.findFirst({
where: {
name: instance.instanceName,
id: instance.instanceId,
},
});
@ -661,11 +228,12 @@ export class TypebotController extends ChatbotController implements ChatbotContr
},
});
// Use the original processTypebot method with all parameters
await this.typebotService.processTypebot(
instanceData,
this.waMonitor.waInstances[instanceData.name],
remoteJid,
null,
null,
null, // msg
null, // session
findBot,
url,
expire,
@ -722,7 +290,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
request.data.clientSideActions,
);
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
remoteJid: remoteJid,
url: url,
typebot: typebot,
@ -747,325 +315,4 @@ export class TypebotController extends ChatbotController implements ChatbotContr
},
};
}
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const remoteJid = data.remoteJid;
const status = data.status;
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
instanceId: instanceId,
botId: { not: null },
},
});
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
instanceId: instanceId,
botId: { not: null },
},
});
}
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
}
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const typebotData = {
remoteJid: remoteJid,
status: status,
session,
};
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
return { typebot: { ...instance, typebot: typebotData } };
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const typebot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (typebot && typebot.instanceId !== instanceId) {
throw new Error('Typebot not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: botId ?? { not: null },
type: 'typebot',
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async emit({
instance,
remoteJid,
msg,
}: {
instance: InstanceDto;
remoteJid: string;
msg: any;
pushName?: string;
}) {
if (!this.integrationEnabled) return;
try {
const instanceData = await this.prismaRepository.instance.findFirst({
where: {
name: instance.instanceName,
},
});
if (!instanceData) throw new Error('Instance not found');
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as TypebotModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.typebotIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.typebotIdFallback,
},
});
findBot = findFallback;
} else {
return;
}
}
const settings = await this.prismaRepository.typebotSetting.findFirst({
where: {
instanceId: instance.instanceId,
},
});
const url = findBot?.url;
const typebot = findBot?.typebot;
let expire = findBot?.expire;
let keywordFinish = findBot?.keywordFinish;
let delayMessage = findBot?.delayMessage;
let unknownMessage = findBot?.unknownMessage;
let listeningFromMe = findBot?.listeningFromMe;
let stopBotFromMe = findBot?.stopBotFromMe;
let keepOpen = findBot?.keepOpen;
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (this.checkIgnoreJids(ignoreJids, remoteJid)) return;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.sessionRepository.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.typebotService.processTypebot(
instanceData,
remoteJid,
msg,
session,
findBot,
url,
expire,
typebot,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debouncedContent,
);
});
} else {
await this.typebotService.processTypebot(
instanceData,
remoteJid,
msg,
session,
findBot,
url,
expire,
typebot,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
content,
);
}
if (session && !session.awaitUser) return;
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

@ -1,4 +1,4 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class PrefilledVariables {
remoteJid?: string;
@ -7,34 +7,11 @@ export class PrefilledVariables {
additionalData?: { [key: string]: any };
}
export class TypebotDto {
enabled?: boolean;
description?: string;
export class TypebotDto extends BaseChatbotDto {
url: string;
typebot?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
typebot: string;
}
export class TypebotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
export class TypebotSettingDto extends BaseChatbotSettingDto {
typebotIdFallback?: string;
ignoreJids?: any;
}

View File

@ -132,6 +132,7 @@ export class EventController {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@ -151,5 +152,8 @@ export class EventController {
'TYPEBOT_CHANGE_STATUS',
'REMOVE_INSTANCE',
'LOGOUT_INSTANCE',
'INSTANCE_CREATE',
'INSTANCE_DELETE',
'STATUS_INSTANCE',
];
}

View File

@ -26,6 +26,11 @@ export class EventDto {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;
@ -63,6 +68,11 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;

View File

@ -1,3 +1,4 @@
import { NatsController } from '@api/integrations/event/nats/nats.controller';
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
@ -13,6 +14,7 @@ export class EventManager {
private websocketController: WebsocketController;
private webhookController: WebhookController;
private rabbitmqController: RabbitmqController;
private natsController: NatsController;
private sqsController: SqsController;
private pusherController: PusherController;
@ -23,6 +25,7 @@ export class EventManager {
this.websocket = new WebsocketController(prismaRepository, waMonitor);
this.webhook = new WebhookController(prismaRepository, waMonitor);
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
this.nats = new NatsController(prismaRepository, waMonitor);
this.sqs = new SqsController(prismaRepository, waMonitor);
this.pusher = new PusherController(prismaRepository, waMonitor);
}
@ -67,6 +70,14 @@ export class EventManager {
return this.rabbitmqController;
}
public set nats(nats: NatsController) {
this.natsController = nats;
}
public get nats() {
return this.natsController;
}
public set sqs(sqs: SqsController) {
this.sqsController = sqs;
}
@ -85,6 +96,7 @@ export class EventManager {
public init(httpServer: Server): void {
this.websocket.init(httpServer);
this.rabbitmq.init();
this.nats.init();
this.sqs.init();
this.pusher.init();
}
@ -103,6 +115,7 @@ export class EventManager {
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);
await this.nats.emit(eventData);
await this.sqs.emit(eventData);
await this.webhook.emit(eventData);
await this.pusher.emit(eventData);
@ -125,6 +138,14 @@ export class EventManager {
},
});
if (data.nats)
await this.nats.set(instanceName, {
nats: {
enabled: true,
events: data.nats?.events,
},
});
if (data.sqs)
await this.sqs.set(instanceName, {
sqs: {

View File

@ -1,3 +1,4 @@
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
@ -14,6 +15,7 @@ export class EventRouter {
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
this.router.use('/websocket', new WebsocketRouter(...guards).router);
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
this.router.use('/nats', new NatsRouter(...guards).router);
this.router.use('/pusher', new PusherRouter(...guards).router);
this.router.use('/sqs', new SqsRouter(...guards).router);
}

View File

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

View File

@ -0,0 +1,161 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Log, Nats } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { connect, NatsConnection, StringCodec } from 'nats';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
export class NatsController extends EventController implements EventControllerInterface {
public natsClient: NatsConnection | null = null;
private readonly logger = new Logger('NatsController');
private readonly sc = StringCodec();
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Nats>('NATS')?.ENABLED, 'nats');
}
public async init(): Promise<void> {
if (!this.status) {
return;
}
try {
const uri = configService.get<Nats>('NATS').URI;
this.natsClient = await connect({ servers: uri });
this.logger.info('NATS initialized');
if (configService.get<Nats>('NATS')?.GLOBAL_ENABLED) {
await this.initGlobalSubscriptions();
}
} catch (error) {
this.logger.error('Failed to connect to NATS:');
this.logger.error(error);
throw error;
}
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('nats')) {
return;
}
if (!this.status || !this.natsClient) {
return;
}
const instanceNats = await this.get(instanceName);
const natsLocal = instanceNats?.events;
const natsGlobal = configService.get<Nats>('NATS').GLOBAL_ENABLED;
const natsEvents = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
event,
instance: instanceName,
data,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
// Instância específica
if (instanceNats?.enabled) {
if (Array.isArray(natsLocal) && natsLocal.includes(we)) {
const subject = `${instanceName}.${event.toLowerCase()}`;
try {
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (instance): ${error}`);
}
}
}
// Global
if (natsGlobal && natsEvents[we]) {
try {
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS-Global`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (global): ${error}`);
}
}
}
private async initGlobalSubscriptions(): Promise<void> {
this.logger.info('Initializing global subscriptions');
const events = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
if (!events) {
this.logger.warn('No events to initialize on NATS');
return;
}
const eventKeys = Object.keys(events);
for (const event of eventKeys) {
if (events[event] === false) continue;
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
// Criar uma subscription para cada evento
try {
const subscription = this.natsClient.subscribe(subject);
this.logger.info(`Subscribed to: ${subject}`);
// Processar mensagens (exemplo básico)
(async () => {
for await (const msg of subscription) {
try {
const data = JSON.parse(this.sc.decode(msg.data));
// Aqui você pode adicionar a lógica de processamento
this.logger.debug(`Received message on ${subject}:`);
this.logger.debug(data);
} catch (error) {
this.logger.error(`Error processing message on ${subject}:`);
this.logger.error(error);
}
}
})();
} catch (error) {
this.logger.error(`Failed to subscribe to ${subject}:`);
this.logger.error(error);
}
}
}
}

View File

@ -0,0 +1,36 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { EventDto } from '@api/integrations/event/event.dto';
import { HttpStatus } from '@api/routes/index.router';
import { eventManager } from '@api/server.module';
import { eventSchema, instanceSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class NatsRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {
const response = await this.dataValidate<EventDto>({
request: req,
schema: eventSchema,
ClassRef: EventDto,
execute: (instance, data) => eventManager.nats.set(instance.instanceName, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => eventManager.nats.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -21,9 +21,21 @@ export class RabbitmqController extends EventController implements EventControll
await new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
amqp.connect(uri, (error, connection) => {
const url = new URL(uri);
const connectionOptions = {
protocol: url.protocol.slice(0, -1),
hostname: url.hostname,
port: url.port || 5672,
username: url.username || 'guest',
password: url.password || 'guest',
vhost: url.pathname.slice(1) || '/',
frameMax: frameMax,
};
amqp.connect(connectionOptions, (error, connection) => {
if (error) {
reject(error);

View File

@ -1,10 +1,11 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { SQS } from '@aws-sdk/client-sqs';
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
import { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
import { EventDto } from '../event.dto';
export class SqsController extends EventController implements EventControllerInterface {
private sqs: SQS;
@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt
return this.sqs;
}
override async set(instanceName: string, data: EventDto): Promise<any> {
if (!this.status) {
return;
}
if (!data[this.name]?.enabled) {
data[this.name].events = [];
} else {
if (0 === data[this.name].events.length) {
data[this.name].events = EventController.events;
}
}
await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled);
const payload: any = {
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
update: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
},
create: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
};
console.log('*** payload: ', payload);
return this.prisma[this.name].upsert(payload);
}
public async emit({
instanceName,
origin,
@ -121,70 +155,92 @@ export class SqsController extends EventController implements EventControllerInt
}
}
public async initQueues(instanceName: string, events: string[]) {
if (!events || !events.length) return;
private async saveQueues(instanceName: string, events: string[], enable: boolean) {
if (enable) {
const eventsFinded = await this.listQueuesByInstance(instanceName);
console.log('eventsFinded', eventsFinded);
const queues = events.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
for (const event of events) {
const normalizedEvent = event.toLowerCase();
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
if (eventsFinded.includes(normalizedEvent)) {
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
continue;
}
this.sqs.createQueue(
{
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
},
(err, data) => {
if (err) {
this.logger.error(`Error creating queue ${queueName}: ${err.message}`);
} else {
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
}
},
);
});
const queueName = `${instanceName}_${normalizedEvent}.fifo`;
try {
const createCommand = new CreateQueueCommand({
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
});
const data = await this.sqs.send(createCommand);
this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
} catch (err: any) {
this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
}
}
}
}
public async removeQueues(instanceName: string, events: any) {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
private async listQueuesByInstance(instanceName: string) {
let existingQueues: string[] = [];
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
if (listData.QueueUrls && listData.QueueUrls.length > 0) {
// Extrai o nome da fila a partir da URL
existingQueues = listData.QueueUrls.map((queueUrl) => {
const parts = queueUrl.split('/');
return parts[parts.length - 1];
});
}
} catch (error: any) {
this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`);
return;
}
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
// Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
return existingQueues
.map((queueName) => {
// Espera-se que o nome seja `${instanceName}_${event}.fifo`
if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) {
return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase();
}
return '';
})
.filter((event) => event !== '');
}
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
// Para uma futura feature de exclusão forçada das queues
private async removeQueuesByInstance(instanceName: string) {
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
this.sqs.getQueueUrl(
{
QueueName: queueName,
},
(err, data) => {
if (err) {
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
} else {
const queueUrl = data.QueueUrl;
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
this.logger.info(`No queues found for instance ${instanceName}`);
return;
}
this.sqs.deleteQueue(
{
QueueUrl: queueUrl,
},
(deleteErr) => {
if (deleteErr) {
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
} else {
this.logger.info(`Queue ${queueName} deleted`);
}
},
);
}
},
);
});
for (const queueUrl of listData.QueueUrls) {
try {
const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
await this.sqs.send(deleteCommand);
this.logger.info(`Queue ${queueUrl} deleted`);
} catch (err: any) {
this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
}
}
} catch (err: any) {
this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`);
}
}
}

View File

@ -4,9 +4,9 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
// import { BadRequestException } from '@exceptions';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import * as jwt from 'jsonwebtoken';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
@ -18,9 +18,9 @@ export class WebhookController extends EventController implements EventControlle
}
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
if (!isURL(data.webhook.url, { require_tld: false })) {
throw new BadRequestException('Invalid "url" property');
}
// if (!/^(https?:\/\/)/.test(data.webhook.url)) {
// throw new BadRequestException('Invalid "url" property');
// }
if (!data.webhook?.enabled) {
data.webhook.events = [];
@ -74,10 +74,20 @@ export class WebhookController extends EventController implements EventControlle
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
const webhookHeaders = { ...((instance?.headers as Record<string, string>) || {}) };
if (webhookHeaders && 'jwt_key' in webhookHeaders) {
const jwtKey = webhookHeaders['jwt_key'];
const jwtToken = this.generateJwtToken(jwtKey);
webhookHeaders['Authorization'] = `Bearer ${jwtToken}`;
delete webhookHeaders['jwt_key'];
}
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const regex = /^(https?:\/\/)/;
const webhookData = {
event,
@ -111,10 +121,11 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
if (instance?.enabled && regex.test(instance.url)) {
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
@ -155,8 +166,11 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
if (regex.test(globalURL)) {
const httpService = axios.create({
baseURL: globalURL,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
await this.retryWebhookRequest(
httpService,
@ -190,12 +204,20 @@ export class WebhookController extends EventController implements EventControlle
origin: string,
baseURL: string,
serverUrl: string,
maxRetries = 10,
delaySeconds = 30,
maxRetries?: number,
delaySeconds?: number,
): Promise<void> {
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10;
const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5;
const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true;
const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300;
const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2;
const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422];
let attempts = 0;
while (attempts < maxRetries) {
while (attempts < maxRetryAttempts) {
try {
await httpService.post('', webhookData);
if (attempts > 0) {
@ -209,12 +231,27 @@ export class WebhookController extends EventController implements EventControlle
} catch (error) {
attempts++;
const isTimeout = error.code === 'ECONNABORTED';
if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) {
this.logger.error({
local: `${origin}`,
message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`,
statusCode: error?.response?.status,
url: baseURL,
server_url: serverUrl,
});
throw error;
}
this.logger.error({
local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
isTimeout,
statusCode: error?.response?.status,
error: error?.errno,
stack: error?.stack,
name: error?.name,
@ -222,12 +259,46 @@ export class WebhookController extends EventController implements EventControlle
server_url: serverUrl,
});
if (attempts === maxRetries) {
if (attempts === maxRetryAttempts) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
let nextDelay = initialDelay;
if (useExponentialBackoff) {
nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay);
const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1);
nextDelay = Math.max(initialDelay, nextDelay + jitter);
}
this.logger.log({
local: `${origin}`,
message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`,
url: baseURL,
});
await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000));
}
}
}
private generateJwtToken(authToken: string): string {
try {
const payload = {
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration
app: 'evolution',
action: 'webhook',
};
const token = jwt.sign(payload, authToken, { algorithm: 'HS256' });
return token;
} catch (error) {
this.logger.error({
local: 'WebhookController.generateJwtToken',
message: `JWT generation failed: ${error?.message}`,
});
throw error;
}
}
}

View File

@ -1,6 +1,6 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Cors, Log, Websocket } from '@config/env.config';
import { Auth, configService, Cors, Log, Websocket } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Server } from 'http';
import { Server as SocketIO } from 'socket.io';
@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl
}
this.socket = new SocketIO(httpServer, {
cors: {
origin: this.cors,
cors: { origin: this.cors },
allowRequest: async (req, callback) => {
try {
const url = new URL(req.url || '', 'http://localhost');
const params = new URLSearchParams(url.search);
// Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4)
if (params.has('EIO')) {
return callback(null, true);
}
const apiKey = params.get('apikey') || (req.headers.apikey as string);
if (!apiKey) {
this.logger.error('Connection rejected: apiKey not provided');
return callback('apiKey is required', false);
}
const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } });
if (!instance) {
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
if (apiKey !== globalToken) {
this.logger.error('Connection rejected: invalid global token');
return callback('Invalid global token', false);
}
}
callback(null, true);
} catch (error) {
this.logger.error('Authentication error:');
this.logger.error(error);
callback('Authentication error', false);
}
},
});
@ -101,10 +133,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-WebsocketGlobal`,
...message,
});
this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
}
}
@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.of(`/${instanceName}`).emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-Websocket`,
...message,
});
this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
}
}
} catch (err) {

View File

@ -63,9 +63,9 @@ const createBucket = async () => {
if (!exists) {
await minioClient.makeBucket(bucketName);
}
await setBucketPolicy();
if (!BUCKET.SKIP_POLICY) {
await setBucketPolicy();
}
logger.info(`S3 Bucket ${bucketName} - ON`);
return true;
} catch (error) {

View File

@ -1,7 +1,7 @@
import { Auth, ConfigService, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import axios from 'axios';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
type ResponseSuccess = { status: number; data?: any };
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
@ -36,7 +36,7 @@ export class ProviderFiles {
} catch (error) {
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
const pid = process.pid;
execSync(`kill -9 ${pid}`);
execFileSync('kill', ['-9', `${pid}`]);
}
}
}

View File

@ -0,0 +1,37 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { NumberDto } from '@api/dto/chat.dto';
import { businessController } from '@api/server.module';
import { catalogSchema, collectionsSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class BusinessRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('getCatalog'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: catalogSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCatalog(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getCollections'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: collectionsSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCollections(instance, data),
});
return res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -46,14 +46,19 @@ export class ChatRouter extends RouterBroker {
super();
this.router
.post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => {
const response = await this.dataValidate<WhatsAppNumberDto>({
request: req,
schema: whatsappNumberSchema,
ClassRef: WhatsAppNumberDto,
execute: (instance, data) => chatController.whatsappNumber(instance, data),
});
try {
const response = await this.dataValidate<WhatsAppNumberDto>({
request: req,
schema: whatsappNumberSchema,
ClassRef: WhatsAppNumberDto,
execute: (instance, data) => chatController.whatsappNumber(instance, data),
});
return res.status(HttpStatus.OK).json(response);
return res.status(HttpStatus.OK).json(response);
} catch (error) {
console.log(error);
return res.status(HttpStatus.BAD_REQUEST).json(error);
}
})
.post(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => {
const response = await this.dataValidate<ReadMessageDto>({
@ -207,7 +212,6 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileNameDto>({
request: req,

View File

@ -6,11 +6,13 @@ import { ChatbotRouter } from '@api/integrations/chatbot/chatbot.router';
import { EventRouter } from '@api/integrations/event/event.router';
import { StorageRouter } from '@api/integrations/storage/storage.router';
import { configService } from '@config/env.config';
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
import { Router } from 'express';
import fs from 'fs';
import mimeTypes from 'mime-types';
import path from 'path';
import { BusinessRouter } from './business.router';
import { CallRouter } from './call.router';
import { ChatRouter } from './chat.router';
import { GroupRouter } from './group.router';
@ -59,7 +61,7 @@ router.get('/assets/*', (req, res) => {
router
.use((req, res, next) => telemetry.collectTelemetry(req, res, next))
.get('/', (req, res) => {
.get('/', async (req, res) => {
res.status(HttpStatus.OK).json({
status: HttpStatus.OK,
message: 'Welcome to the Evolution API, it is working!',
@ -67,6 +69,8 @@ router
clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME,
manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined,
documentation: `https://doc.evolution-api.com`,
whatsappWebVersion:
process.env.CONFIG_SESSION_PHONE_VERSION || (await fetchLatestWaWebVersion({})).version.join('.'),
});
})
.post('/verify-creds', authGuard['apikey'], async (req, res) => {
@ -82,6 +86,7 @@ router
.use('/message', new MessageRouter(...guards).router)
.use('/call', new CallRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router)
.use('/business', new BusinessRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router)
.use('/template', new TemplateRouter(configService, ...guards).router)
.use('/settings', new SettingsRouter(...guards).router)

View File

@ -15,7 +15,6 @@ export class InstanceRouter extends RouterBroker {
super();
this.router
.post('/create', ...guards, async (req, res) => {
console.log('create instance', req.body);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,

View File

@ -3,6 +3,7 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
import { eventEmitter } from '@config/event.config';
import { Logger } from '@config/logger.config';
import { BusinessController } from './controllers/business.controller';
import { CallController } from './controllers/call.controller';
import { ChatController } from './controllers/chat.controller';
import { GroupController } from './controllers/group.controller';
@ -21,10 +22,14 @@ import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
import { DifyService } from './integrations/chatbot/dify/services/dify.service';
import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller';
import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service';
import { EvolutionBotController } from './integrations/chatbot/evolutionBot/controllers/evolutionBot.controller';
import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service';
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
import { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
@ -98,6 +103,7 @@ export const instanceController = new InstanceController(
export const sendMessageController = new SendMessageController(waMonitor);
export const callController = new CallController(waMonitor);
export const chatController = new ChatController(waMonitor);
export const businessController = new BusinessController(waMonitor);
export const groupController = new GroupController(waMonitor);
export const labelController = new LabelController(waMonitor);
@ -109,20 +115,27 @@ export const channelController = new ChannelController(prismaRepository, waMonit
export const evolutionController = new EvolutionController(prismaRepository, waMonitor);
export const metaController = new MetaController(prismaRepository, waMonitor);
export const baileysController = new BaileysController(waMonitor);
// chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
const openaiService = new OpenaiService(waMonitor, configService, prismaRepository);
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
const difyService = new DifyService(waMonitor, configService, prismaRepository);
// chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository);
const evolutionBotService = new EvolutionBotService(waMonitor, prismaRepository, configService, openaiService);
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository);
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService, openaiService);
export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor);
logger.info('Module - ON');

View File

@ -45,11 +45,11 @@ export class ChannelStartupService {
this.chatwootCache,
);
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository);
public openaiService = new OpenaiService(waMonitor, this.prismaRepository, this.configService);
public openaiService = new OpenaiService(waMonitor, this.configService, this.prismaRepository);
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository);
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceName);
@ -503,8 +503,29 @@ export class ChannelStartupService {
where['remoteJid'] = remoteJid;
}
return await this.prismaRepository.contact.findMany({
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
where,
};
if (query.offset) contactFindManyArgs.take = query.offset;
if (query.page) {
const validPage = Math.max(query.page as number, 1);
contactFindManyArgs.skip = query.offset * (validPage - 1);
}
const contacts = await this.prismaRepository.contact.findMany(contactFindManyArgs);
return contacts.map((contact) => {
const remoteJid = contact.remoteJid;
const isGroup = remoteJid.endsWith('@g.us');
const isSaved = !!contact.pushName || !!contact.profilePicUrl;
const type = isGroup ? 'group' : isSaved ? 'contact' : 'group_member';
return {
...contact,
isGroup,
isSaved,
type,
};
});
}
@ -656,6 +677,14 @@ export class ChannelStartupService {
}
public async fetchStatusMessage(query: any) {
if (!query?.offset) {
query.offset = 50;
}
if (!query?.page) {
query.page = 1;
}
return await this.prismaRepository.messageUpdate.findMany({
where: {
instanceId: this.instanceId,
@ -685,88 +714,100 @@ export class ChannelStartupService {
const timestampFilter =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
const limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``;
const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : Prisma.sql``;
const results = await this.prismaRepository.$queryRaw`
WITH rankedMessages AS (
SELECT DISTINCT ON ("Contact"."remoteJid")
"Contact"."id",
"Contact"."remoteJid",
"Contact"."pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
CASE
WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true
ELSE false
END as "windowActive",
"Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key,
"Message"."pushName" AS lastMessagePushName,
"Message"."participant" AS lastMessageParticipant,
"Message"."messageType" AS lastMessageMessageType,
"Message"."message" AS lastMessageMessage,
"Message"."contextInfo" AS lastMessageContextInfo,
"Message"."source" AS lastMessageSource,
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
"Message"."instanceId" AS lastMessageInstanceId,
"Message"."sessionId" AS lastMessageSessionId,
"Message"."status" AS lastMessageStatus
FROM "Contact"
INNER JOIN "Message" ON "Message"."key"->>'remoteJid' = "Contact"."remoteJid"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Contact"."remoteJid"
AND "Chat"."instanceId" = "Contact"."instanceId"
WHERE
"Contact"."instanceId" = ${this.instanceId}
AND "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Contact"."remoteJid" = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY
"Contact"."remoteJid",
"Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST;
WITH rankedMessages AS (
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
"Contact"."id" as "contactId",
"Message"."key"->>'remoteJid' as "remoteJid",
CASE
WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName")
ELSE COALESCE("Contact"."pushName", "Message"."pushName")
END as "pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."name" as "pushName",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
"Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key,
CASE
WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você'
ELSE "Message"."pushName"
END AS lastMessagePushName,
"Message"."participant" AS lastMessageParticipant,
"Message"."messageType" AS lastMessageMessageType,
"Message"."message" AS lastMessageMessage,
"Message"."contextInfo" AS lastMessageContextInfo,
"Message"."source" AS lastMessageSource,
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
"Message"."instanceId" AS lastMessageInstanceId,
"Message"."sessionId" AS lastMessageSessionId,
"Message"."status" AS lastMessageStatus
FROM "Message"
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
WHERE "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST
${limit}
${offset};
`;
if (results && isArray(results) && results.length > 0) {
const mappedResults = results.map((contact) => {
const lastMessage = contact.lastMessageId
const lastMessage = contact.lastmessageid
? {
id: contact.lastMessageId,
key: contact.lastMessageKey,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
id: contact.lastmessageid,
key: contact.lastmessage_key,
pushName: contact.lastmessagepushname,
participant: contact.lastmessageparticipant,
messageType: contact.lastmessagemessagetype,
message: contact.lastmessagemessage,
contextInfo: contact.lastmessagecontextinfo,
source: contact.lastmessagesource,
messageTimestamp: contact.lastmessagemessagetimestamp,
instanceId: contact.lastmessageinstanceid,
sessionId: contact.lastmessagesessionid,
status: contact.lastmessagestatus,
}
: undefined;
return {
id: contact.id,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
id: contact.contactid || null,
remoteJid: contact.remotejid,
pushName: contact.pushname,
profilePicUrl: contact.profilepicurl,
updatedAt: contact.updatedat,
windowStart: contact.windowstart,
windowExpires: contact.windowexpires,
windowActive: contact.windowactive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0,
isSaved: !!contact.contactid,
};
});
if (query?.take && query?.skip) {
const skip = query.skip || 0;
const take = query.take || 20;
return mappedResults.slice(skip, skip + take);
}
return mappedResults;
}

View File

@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs';
import { join } from 'path';
@ -91,6 +91,7 @@ export class WAMonitoringService {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
Sqs: true,
Websocket: true,
Setting: true,
@ -168,7 +169,8 @@ export class WAMonitoringService {
public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
execFileSync('rm', ['-rf', instancePath]);
}
const instance = await this.prismaRepository.instance.findFirst({
@ -190,6 +192,7 @@ export class WAMonitoringService {
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });

View File

@ -15,6 +15,7 @@ export enum Events {
MESSAGES_UPDATE = 'messages.update',
MESSAGES_DELETE = 'messages.delete',
SEND_MESSAGE = 'send.message',
SEND_MESSAGE_UPDATE = 'send.message.update',
CONTACTS_SET = 'contacts.set',
CONTACTS_UPSERT = 'contacts.upsert',
CONTACTS_UPDATE = 'contacts.update',

View File

@ -72,6 +72,7 @@ export type EventsRabbitmq = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -94,10 +95,20 @@ export type EventsRabbitmq = {
export type Rabbitmq = {
ENABLED: boolean;
URI: string;
FRAME_MAX: number;
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY: string;
PREFIX_KEY?: string;
};
export type Nats = {
ENABLED: boolean;
URI: string;
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY?: string;
};
export type Sqs = {
@ -131,6 +142,7 @@ export type EventsWebhook = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -163,6 +175,7 @@ export type EventsPusher = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -220,7 +233,21 @@ export type CacheConfLocal = {
TTL: number;
};
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
export type Webhook = {
GLOBAL?: GlobalWebhook;
EVENTS: EventsWebhook;
REQUEST?: {
TIMEOUT_MS?: number;
};
RETRY?: {
MAX_ATTEMPTS?: number;
INITIAL_DELAY_SECONDS?: number;
USE_EXPONENTIAL_BACKOFF?: boolean;
MAX_DELAY_SECONDS?: number;
JITTER_FACTOR?: number;
NON_RETRYABLE_STATUS_CODES?: number[];
};
};
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
export type QrCode = { LIMIT: number; COLOR: string };
@ -241,6 +268,9 @@ export type Chatwoot = {
};
export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
export type Dify = { ENABLED: boolean };
export type N8n = { ENABLED: boolean };
export type Evoai = { ENABLED: boolean };
export type Flowise = { ENABLED: boolean };
export type S3 = {
ACCESS_KEY: string;
@ -251,6 +281,7 @@ export type S3 = {
PORT?: number;
USE_SSL?: boolean;
REGION?: string;
SKIP_POLICY?: boolean;
};
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
@ -263,6 +294,7 @@ export interface Env {
PROVIDER: ProviderSession;
DATABASE: Database;
RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
@ -278,6 +310,9 @@ export interface Env {
CHATWOOT: Chatwoot;
OPENAI: Openai;
DIFY: Dify;
N8N: N8n;
EVOAI: Evoai;
FLOWISE: Flowise;
CACHE: CacheConf;
S3?: S3;
AUTHENTICATION: Auth;
@ -356,9 +391,10 @@ export class ConfigService {
RABBITMQ: {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || 'evolution',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY,
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.RABBITMQ_URI || '',
FRAME_MAX: Number.parseInt(process.env.RABBITMQ_FRAME_MAX) || 8192,
EVENTS: {
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',
@ -370,6 +406,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true',
@ -389,6 +426,43 @@ export class ConfigService {
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
NATS: {
ENABLED: process.env?.NATS_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.NATS_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.NATS_PREFIX_KEY,
EXCHANGE_NAME: process.env?.NATS_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.NATS_URI || '',
EVENTS: {
APPLICATION_STARTUP: process.env?.NATS_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.NATS_EVENTS_INSTANCE_CREATE === 'true',
INSTANCE_DELETE: process.env?.NATS_EVENTS_INSTANCE_DELETE === 'true',
QRCODE_UPDATED: process.env?.NATS_EVENTS_QRCODE_UPDATED === 'true',
MESSAGES_SET: process.env?.NATS_EVENTS_MESSAGES_SET === 'true',
MESSAGES_UPSERT: process.env?.NATS_EVENTS_MESSAGES_UPSERT === 'true',
MESSAGES_EDITED: process.env?.NATS_EVENTS_MESSAGES_EDITED === 'true',
MESSAGES_UPDATE: process.env?.NATS_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.NATS_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.NATS_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.NATS_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.NATS_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.NATS_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.NATS_EVENTS_CONTACTS_UPSERT === 'true',
PRESENCE_UPDATE: process.env?.NATS_EVENTS_PRESENCE_UPDATE === 'true',
CHATS_SET: process.env?.NATS_EVENTS_CHATS_SET === 'true',
CHATS_UPDATE: process.env?.NATS_EVENTS_CHATS_UPDATE === 'true',
CHATS_UPSERT: process.env?.NATS_EVENTS_CHATS_UPSERT === 'true',
CHATS_DELETE: process.env?.NATS_EVENTS_CHATS_DELETE === 'true',
CONNECTION_UPDATE: process.env?.NATS_EVENTS_CONNECTION_UPDATE === 'true',
LABELS_EDIT: process.env?.NATS_EVENTS_LABELS_EDIT === 'true',
LABELS_ASSOCIATION: process.env?.NATS_EVENTS_LABELS_ASSOCIATION === 'true',
GROUPS_UPSERT: process.env?.NATS_EVENTS_GROUPS_UPSERT === 'true',
GROUP_UPDATE: process.env?.NATS_EVENTS_GROUPS_UPDATE === 'true',
GROUP_PARTICIPANTS_UPDATE: process.env?.NATS_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
CALL: process.env?.NATS_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
SQS: {
ENABLED: process.env?.SQS_ENABLED === 'true',
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '',
@ -421,6 +495,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.PUSHER_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === 'true',
@ -477,6 +552,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true',
@ -497,6 +573,19 @@ export class ConfigService {
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
},
REQUEST: {
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
},
RETRY: {
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [
400, 401, 403, 404, 422,
],
},
},
CONFIG_SESSION_PHONE: {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
@ -533,6 +622,15 @@ export class ConfigService {
DIFY: {
ENABLED: process.env?.DIFY_ENABLED === 'true',
},
N8N: {
ENABLED: process.env?.N8N_ENABLED === 'true',
},
EVOAI: {
ENABLED: process.env?.EVOAI_ENABLED === 'true',
},
FLOWISE: {
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
},
CACHE: {
REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
@ -555,6 +653,7 @@ export class ConfigService {
PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
USE_SSL: process.env?.S3_USE_SSL === 'true',
REGION: process.env?.S3_REGION,
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
},
AUTHENTICATION: {
API_KEY: {

View File

@ -128,7 +128,15 @@ async function bootstrap() {
const httpServer = configService.get<HttpServer>('SERVER');
ServerUP.app = app;
const server = ServerUP[httpServer.TYPE];
let server = ServerUP[httpServer.TYPE];
if (server === null) {
logger.warn('SSL cert load failed — falling back to HTTP.');
logger.info("Ensure 'SSL_CONF_PRIVKEY' and 'SSL_CONF_FULLCHAIN' env vars point to valid certificate files.");
httpServer.TYPE = 'http';
server = ServerUP[httpServer.TYPE];
}
eventManager.init(server);

View File

@ -0,0 +1,37 @@
import axios, { AxiosRequestConfig } from 'axios';
import { fetchLatestBaileysVersion, WAVersion } from 'baileys';
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
try {
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
...options,
responseType: 'json',
});
const regex = /\\?"client_revision\\?":\s*(\d+)/;
const match = data.match(regex);
if (!match?.[1]) {
return {
version: (await fetchLatestBaileysVersion()).version as WAVersion,
isLatest: false,
error: {
message: 'Could not find client revision in the fetched content',
},
};
}
const clientRevision = match[1];
return {
version: [2, 3000, +clientRevision] as WAVersion,
isLatest: true,
};
} catch (error) {
return {
version: (await fetchLatestBaileysVersion()).version as WAVersion,
isLatest: false,
error,
};
}
};

Some files were not shown because too many files have changed in this diff Show More