Merge branch 'release/2.3.0'

This commit is contained in:
Davidson Gomes 2025-06-17 09:25:38 -03:00
commit 39606240da
107 changed files with 9406 additions and 10007 deletions

View File

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

View File

@ -3,6 +3,9 @@ SERVER_PORT=8080
# Server URL - Set your application url # Server URL - Set your application url
SERVER_URL=http://localhost:8080 SERVER_URL=http://localhost:8080
SSL_CONF_PRIVKEY=/path/to/cert.key
SSL_CONF_FULLCHAIN=/path/to/cert.crt
SENTRY_DSN= SENTRY_DSN=
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' # 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_ENABLED=false
RABBITMQ_URI=amqp://localhost RABBITMQ_URI=amqp://localhost
RABBITMQ_EXCHANGE_NAME=evolution 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. # Global events - By enabling this variable, events from all instances are sent in the same event queue.
RABBITMQ_GLOBAL_ENABLED=false RABBITMQ_GLOBAL_ENABLED=false
# Prefix key to queue name # Prefix key to queue name
@ -62,6 +66,7 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
RABBITMQ_EVENTS_MESSAGES_UPDATE=false RABBITMQ_EVENTS_MESSAGES_UPDATE=false
RABBITMQ_EVENTS_MESSAGES_DELETE=false RABBITMQ_EVENTS_MESSAGES_DELETE=false
RABBITMQ_EVENTS_SEND_MESSAGE=false RABBITMQ_EVENTS_SEND_MESSAGE=false
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
RABBITMQ_EVENTS_CONTACTS_SET=false RABBITMQ_EVENTS_CONTACTS_SET=false
RABBITMQ_EVENTS_CONTACTS_UPSERT=false RABBITMQ_EVENTS_CONTACTS_UPSERT=false
RABBITMQ_EVENTS_CONTACTS_UPDATE=false RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -108,6 +113,7 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
PUSHER_EVENTS_MESSAGES_UPDATE=true PUSHER_EVENTS_MESSAGES_UPDATE=true
PUSHER_EVENTS_MESSAGES_DELETE=true PUSHER_EVENTS_MESSAGES_DELETE=true
PUSHER_EVENTS_SEND_MESSAGE=true PUSHER_EVENTS_SEND_MESSAGE=true
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
PUSHER_EVENTS_CONTACTS_SET=true PUSHER_EVENTS_CONTACTS_SET=true
PUSHER_EVENTS_CONTACTS_UPSERT=true PUSHER_EVENTS_CONTACTS_UPSERT=true
PUSHER_EVENTS_CONTACTS_UPDATE=true PUSHER_EVENTS_CONTACTS_UPDATE=true
@ -149,6 +155,7 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
WEBHOOK_EVENTS_MESSAGES_UPDATE=true WEBHOOK_EVENTS_MESSAGES_UPDATE=true
WEBHOOK_EVENTS_MESSAGES_DELETE=true WEBHOOK_EVENTS_MESSAGES_DELETE=true
WEBHOOK_EVENTS_SEND_MESSAGE=true WEBHOOK_EVENTS_SEND_MESSAGE=true
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
WEBHOOK_EVENTS_CONTACTS_SET=true WEBHOOK_EVENTS_CONTACTS_SET=true
WEBHOOK_EVENTS_CONTACTS_UPSERT=true WEBHOOK_EVENTS_CONTACTS_UPSERT=true
WEBHOOK_EVENTS_CONTACTS_UPDATE=true WEBHOOK_EVENTS_CONTACTS_UPDATE=true
@ -173,6 +180,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
WEBHOOK_EVENTS_ERRORS=false WEBHOOK_EVENTS_ERRORS=false
WEBHOOK_EVENTS_ERRORS_WEBHOOK= 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 # Name that will be displayed on smartphone connection
CONFIG_SESSION_PHONE_CLIENT=Evolution API CONFIG_SESSION_PHONE_CLIENT=Evolution API
# Browser Name = Chrome | Firefox | Edge | Opera | Safari # Browser Name = Chrome | Firefox | Edge | Opera | Safari
@ -180,7 +196,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel # Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web # 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 # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30
@ -210,6 +226,12 @@ OPENAI_ENABLED=false
# Dify - Environment variables # Dify - Environment variables
DIFY_ENABLED=false DIFY_ENABLED=false
# n8n - Environment variables
N8N_ENABLED=false
# EvoAI - Environment variables
EVOAI_ENABLED=false
# Cache - Environment variables # Cache - Environment variables
# Redis Cache enabled # Redis Cache enabled
CACHE_REDIS_ENABLED=true CACHE_REDIS_ENABLED=true
@ -266,4 +288,4 @@ LANGUAGE=en
# PROXY_PORT=80 # PROXY_PORT=80
# PROXY_PROTOCOL=http # PROXY_PROTOCOL=http
# PROXY_USERNAME= # PROXY_USERNAME=
# PROXY_PASSWORD= # PROXY_PASSWORD=

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
/dist /dist
/node_modules /node_modules
.cursor*
/Docker/.env /Docker/.env
.vscode .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) # 2.2.3 (2025-02-03 11:52)
### Fixed ### Fixed

View File

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

View File

@ -1,11 +1,11 @@
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
RUN apk update && \ 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 maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@atendai.com" LABEL contact="contato@evolution-api.com"
WORKDIR /evolution 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. 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: 2. As a contributor, you should agree that:

View File

@ -2,6 +2,7 @@
<div align="center"> <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) [![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) [![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) [![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: 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) - [Promovaweb](https://www.youtube.com/@promovaweb)
- [Sandeco](https://www.youtube.com/@canalsandeco)
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG) - [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
- [Francis MNO](https://www.youtube.com/@FrancisMNO) - [Francis MNO](https://www.youtube.com/@FrancisMNO)
- [Pablo Cabral](https://youtube.com/@pablocabral) - [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. 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). 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: services:
api: api:
container_name: evolution_api container_name: evolution_api
image: atendai/evolution-api:homolog image: evoapicloud/evolution-api:latest
restart: always restart: always
depends_on: depends_on:
- redis - 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"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Evolution Manager</title> <title>Evolution Manager</title>
<script type="module" crossorigin src="/assets/index-CFAZX6IV.js"></script> <script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css"> <link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
</head> </head>
<body> <body>
<div id="root"></div> <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", "name": "evolution-api",
"version": "2.2.3", "version": "2.3.0",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js", "main": "./dist/main.js",
"type": "commonjs", "type": "commonjs",
@ -41,7 +41,7 @@
], ],
"author": { "author": {
"name": "Davidson Gomes", "name": "Davidson Gomes",
"email": "contato@atendai.com" "email": "contato@evolution-api.com"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"bugs": { "bugs": {
@ -58,6 +58,7 @@
"@prisma/client": "^6.1.0", "@prisma/client": "^6.1.0",
"@sentry/node": "^8.47.0", "@sentry/node": "^8.47.0",
"amqplib": "^0.10.5", "amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9", "axios": "^1.7.9",
"baileys": "github:EvolutionAPI/Baileys", "baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
@ -75,6 +76,7 @@
"jimp": "^0.16.13", "jimp": "^0.16.13",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"link-preview-js": "^3.0.13", "link-preview-js": "^3.0.13",
"long": "^5.2.3", "long": "^5.2.3",
"mediainfo.js": "^0.3.4", "mediainfo.js": "^0.3.4",
@ -82,6 +84,7 @@
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"minio": "^8.0.3", "minio": "^8.0.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nats": "^2.29.1",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"openai": "^4.77.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 # 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" provider = "mysql"

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy? Proxy Proxy?
Setting Setting? Setting Setting?
Rabbitmq Rabbitmq? Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs? Sqs Sqs?
Websocket Websocket? Websocket Websocket?
Typebot Typebot[] Typebot Typebot[]
@ -99,12 +100,13 @@ model Instance {
Template Template[] Template Template[]
Dify Dify[] Dify Dify[]
DifySetting DifySetting? DifySetting DifySetting?
integrationSessions IntegrationSession[] IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[] EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting? EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[] Flowise Flowise[]
FlowiseSetting FlowiseSetting? FlowiseSetting FlowiseSetting?
Pusher Pusher? Pusher Pusher?
N8n N8n[]
} }
model Session { model Session {
@ -116,18 +118,19 @@ model Session {
} }
model Chat { model Chat {
id String @id @default(cuid()) id String @id @default(cuid())
remoteJid String @db.VarChar(100) remoteJid String @db.VarChar(100)
name String? @db.VarChar(100) name String? @db.VarChar(100)
labels Json? @db.Json labels Json? @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp updatedAt DateTime? @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
unreadMessages Int @default(0) unreadMessages Int @default(0)
@@unique([instanceId, remoteJid])
@@index([instanceId]) @@index([instanceId])
@@index([remoteJid]) @@index([remoteJid])
@@unique([instanceId, remoteJid])
} }
model Contact { model Contact {
@ -170,6 +173,7 @@ model Message {
sessionId String? sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id]) session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId]) @@index([instanceId])
} }
@ -185,6 +189,7 @@ model MessageUpdate {
messageId String messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
@@index([instanceId]) @@index([instanceId])
@@index([messageId]) @@index([messageId])
} }
@ -201,6 +206,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
@@index([instanceId]) @@index([instanceId])
} }
@ -269,6 +275,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
@@index([instanceId]) @@index([instanceId])
} }
@ -282,6 +289,16 @@ model Rabbitmq {
instanceId String @unique 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 { model Sqs {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(false) enabled Boolean @default(false)
@ -382,7 +399,7 @@ model IntegrationSession {
model Media { model Media {
id String @id @default(cuid()) id String @id @default(cuid())
fileName String @unique @db.VarChar(500) fileName String @db.VarChar(500)
type String @db.VarChar(100) type String @db.VarChar(100)
mimetype String @db.VarChar(100) mimetype String @db.VarChar(100)
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
@ -627,3 +644,100 @@ model IsOnWhatsapp {
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @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 # 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" provider = "postgresql"

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy? Proxy Proxy?
Setting Setting? Setting Setting?
Rabbitmq Rabbitmq? Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs? Sqs Sqs?
Websocket Websocket? Websocket Websocket?
Typebot Typebot[] Typebot Typebot[]
@ -99,12 +100,16 @@ model Instance {
Template Template[] Template Template[]
Dify Dify[] Dify Dify[]
DifySetting DifySetting? DifySetting DifySetting?
integrationSessions IntegrationSession[] IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[] EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting? EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[] Flowise Flowise[]
FlowiseSetting FlowiseSetting? FlowiseSetting FlowiseSetting?
Pusher Pusher? Pusher Pusher?
N8n N8n[]
N8nSetting N8nSetting[]
Evoai Evoai[]
EvoaiSetting EvoaiSetting?
} }
model Session { model Session {
@ -125,6 +130,7 @@ model Chat {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
unreadMessages Int @default(0) unreadMessages Int @default(0)
@@index([instanceId]) @@index([instanceId])
@@index([remoteJid]) @@index([remoteJid])
} }
@ -168,6 +174,7 @@ model Message {
sessionId String? sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id]) session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId]) @@index([instanceId])
} }
@ -183,6 +190,7 @@ model MessageUpdate {
messageId String messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
@@index([instanceId]) @@index([instanceId])
@@index([messageId]) @@index([messageId])
} }
@ -199,6 +207,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
@@index([instanceId]) @@index([instanceId])
} }
@ -269,6 +278,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
@@index([instanceId]) @@index([instanceId])
} }
@ -282,6 +292,16 @@ model Rabbitmq {
instanceId String @unique 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 { model Sqs {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean enabled Boolean @default(false) @db.Boolean
@ -337,6 +357,8 @@ model Typebot {
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
TypebotSetting TypebotSetting[] TypebotSetting TypebotSetting[]
@ -354,6 +376,8 @@ model TypebotSetting {
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
typebotIdFallback String? @db.VarChar(100) typebotIdFallback String? @db.VarChar(100)
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id]) Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
@ -363,7 +387,7 @@ model TypebotSetting {
model Media { model Media {
id String @id @default(cuid()) id String @id @default(cuid())
fileName String @unique @db.VarChar(500) fileName String @db.VarChar(500)
type String @db.VarChar(100) type String @db.VarChar(100)
mimetype String @db.VarChar(100) mimetype String @db.VarChar(100)
createdAt DateTime? @default(now()) @db.Date createdAt DateTime? @default(now()) @db.Date
@ -624,6 +648,104 @@ model IsOnWhatsapp {
id String @id @default(cuid()) id String @id @default(cuid())
remoteJid String @unique @db.VarChar(100) remoteJid String @unique @db.VarChar(100)
jidOptions String jidOptions String
lid String? @db.VarChar(100)
createdAt DateTime @default(now()) @db.Timestamp createdAt DateTime @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @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: { rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled, enabled: instanceData?.rabbitmq?.enabled,
}, },
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: { sqs: {
enabled: instanceData?.sqs?.enabled, enabled: instanceData?.sqs?.enabled,
}, },
@ -258,6 +261,9 @@ export class InstanceController {
rabbitmq: { rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled, enabled: instanceData?.rabbitmq?.enabled,
}, },
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: { sqs: {
enabled: instanceData?.sqs?.enabled, enabled: instanceData?.sqs?.enabled,
}, },

View File

@ -18,6 +18,14 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { isBase64, isURL } from 'class-validator'; 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 { export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {} constructor(private readonly waMonitor: WAMonitoringService) {}
@ -81,8 +89,8 @@ export class SendMessageController {
} }
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) { if (!isEmoji(data.reaction)) {
throw new BadRequestException('"reaction" must be an emoji'); throw new BadRequestException('Reaction must be a single emoji or empty string');
} }
return await this.waMonitor.waInstances[instanceName].reactionMessage(data); 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 exists: boolean,
public readonly number: string, public readonly number: string,
public readonly name?: string, public readonly name?: string,
public readonly lid?: string,
) {} ) {}
} }

View File

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

View File

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

View File

@ -28,7 +28,6 @@ import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator'; import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data'; import FormData from 'form-data';
import { createReadStream } from 'fs';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import { join } from 'path'; import { join } from 'path';
@ -147,11 +146,20 @@ export class BusinessStartupService extends ChannelStartupService {
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION; const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`; urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
// Primeiro, obtenha a URL do arquivo
let result = await axios.get(urlServer, { headers }); 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; return result.data;
} catch (e) { } 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]; const message = received.messages[0];
let content: any = message.type + 'Message'; let content: any = message.type + 'Message';
content = { [content]: message[message.type] }; 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; return content;
} }
@ -192,17 +216,77 @@ export class BusinessStartupService extends ChannelStartupService {
} }
private messageTextJson(received: any) { 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]; 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) { if (message.from === received.metadata.phone_number_id) {
content = { content = {
extendedTextMessage: { text: message.text.body }, 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 { } else {
content = { conversation: message.text.body }; 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; return content;
} }
@ -283,6 +367,12 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template': case 'template':
messageType = 'conversation'; messageType = 'conversation';
break; break;
case 'location':
messageType = 'locationMessage';
break;
case 'sticker':
messageType = 'stickerMessage';
break;
default: default:
messageType = 'conversation'; messageType = 'conversation';
break; break;
@ -299,17 +389,36 @@ export class BusinessStartupService extends ChannelStartupService {
if (received.contacts) pushName = received.contacts[0].profile.name; if (received.contacts) pushName = received.contacts[0].profile.name;
if (received.messages) { if (received.messages) {
const message = received.messages[0]; // Añadir esta línea para definir message
const key = { const key = {
id: received.messages[0].id, id: message.id,
remoteJid: this.phoneNumber, 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 = { messageRaw = {
key, key,
pushName, pushName,
message: this.messageMediaJson(received), message: {
contextInfo: this.messageMediaJson(received)?.contextInfo, 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), messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number, messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown', source: 'unknown',
@ -327,7 +436,10 @@ export class BusinessStartupService extends ChannelStartupService {
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers }); 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; 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 size = result.headers['content-length'] || buffer.data.byteLength;
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName); 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.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64'); messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
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) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
} else { } else {
const buffer = await this.downloadMediaMessage(received?.messages[0]); const buffer = await this.downloadMediaMessage(received?.messages[0]);
messageRaw.message.base64 = buffer.toString('base64'); messageRaw.message.base64 = buffer.toString('base64');
// Processar OpenAI speech-to-text para áudio mesmo sem S3
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) { } else if (received?.messages[0].interactive) {
messageRaw = { messageRaw = {
@ -455,37 +637,6 @@ export class BusinessStartupService extends ChannelStartupService {
// await this.client.readMessages([received.key]); // 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.logger.log(messageRaw);
this.sendDataWebhook(Events.MESSAGES_UPSERT, 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({ await this.prismaRepository.message.create({
data: messageRaw, data: messageRaw,
}); });
@ -714,17 +865,54 @@ export class BusinessStartupService extends ChannelStartupService {
} }
protected async eventHandler(content: any) { protected async eventHandler(content: any) {
const database = this.configService.get<Database>('DATABASE'); try {
const settings = await this.findSettings(); // 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) { protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
try { try {
let quoted: any; let quoted: any;
let webhookUrl: any; let webhookUrl: any;
const linkPreview = options?.linkPreview != false ? undefined : false;
if (options?.quoted) { if (options?.quoted) {
const m = options?.quoted; const m = options?.quoted;
@ -792,7 +980,7 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''), to: number.replace(/\D/g, ''),
text: { text: {
body: message['conversation'], body: message['conversation'],
preview_url: linkPreview, preview_url: Boolean(options?.linkPreview),
}, },
}; };
quoted ? (content.context = { message_id: quoted.id }) : content; quoted ? (content.context = { message_id: quoted.id }) : content;
@ -808,9 +996,10 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''), to: number.replace(/\D/g, ''),
[message['mediaType']]: { [message['mediaType']]: {
[message['type']]: message['id'], [message['type']]: message['id'],
preview_url: linkPreview, ...(message['mediaType'] !== 'audio' &&
...(message['fileName'] && !isImage && { filename: message['fileName'] }), message['fileName'] &&
caption: message['caption'], !isImage && { filename: message['fileName'] }),
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
}, },
}; };
quoted ? (content.context = { message_id: quoted.id }) : content; 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); this.logger.error(messageSent);
return messageSent; return messageSent;
} }
@ -975,29 +1164,50 @@ export class BusinessStartupService extends ChannelStartupService {
return res; return res;
} }
private async getIdMedia(mediaMessage: any) { private async getIdMedia(mediaMessage: any, isFile = false) {
const formData = new FormData(); 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 }); const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
formData.append('typeFile', mediaMessage.mimetype);
formData.append('messaging_product', 'whatsapp');
// const fileBuffer = await fs.readFile(mediaMessage.media); formData.append('typeFile', mimetype);
formData.append('messaging_product', 'whatsapp');
// const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype }); const token = this.token;
// formData.append('file', fileBlob);
// formData.append('typeFile', mediaMessage.mimetype);
// formData.append('messaging_product', 'whatsapp');
const headers = { Authorization: `Bearer ${this.token}` }; const headers = { Authorization: `Bearer ${token}` };
const res = await axios.post( const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media', this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
formData, }/${this.number}/media`;
{ headers },
); const res = await axios.post(url, formData, { headers });
return res.data.id; return res.data.id;
} catch (error) {
this.logger.error(error.response.data);
throw new InternalServerErrorException(error?.toString() || error);
}
} }
protected async prepareMediaMessage(mediaMessage: MediaMessage) { protected async prepareMediaMessage(mediaMessage: MediaMessage) {
@ -1070,48 +1280,87 @@ export class BusinessStartupService extends ChannelStartupService {
return mediaSent; return mediaSent;
} }
public async processAudio(audio: string, number: string) { public async processAudio(audio: string, number: string, file: any) {
number = number.replace(/\D/g, ''); number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`; 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 = { if (file) {
fileName: `${hash}.mp3`, formData.append('file', file.buffer, {
mediaType: 'audio', filename: file.originalname,
media: audio, 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); const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id; prepareMedia.id = id;
prepareMedia.type = '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) { public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data }; const message = await this.processAudio(data.audio, data.number, file);
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 audioSent = await this.sendMessageWithTyping( const audioSent = await this.sendMessageWithTyping(
data.number, 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 { PrismaRepository } from '@api/repository/repository.service';
import { import {
difyController, difyController,
evoaiController,
evolutionBotController, evolutionBotController,
flowiseController, flowiseController,
n8nController,
openaiController, openaiController,
typebotController, typebotController,
} from '@api/server.module'; } from '@api/server.module';
@ -97,6 +99,10 @@ export class ChatbotController {
await difyController.emit(emitData); await difyController.emit(emitData);
await n8nController.emit(emitData);
await evoaiController.emit(emitData);
await flowiseController.emit(emitData); await flowiseController.emit(emitData);
} }
@ -173,7 +179,7 @@ export class ChatbotController {
if (session) { if (session) {
if (session.status !== 'closed' && !session.botId) { if (session.status !== 'closed' && !session.botId) {
this.logger.warn('Session is already opened in another integration'); this.logger.warn('Session is already opened in another integration');
return; return null;
} else if (!session.botId) { } else if (!session.botId) {
session = null; session = null;
} }
@ -188,13 +194,13 @@ export class ChatbotController {
instance: InstanceDto, instance: InstanceDto,
session?: IntegrationSession, session?: IntegrationSession,
) { ) {
let findBot: null; let findBot: any = null;
if (!session) { if (!session) {
findBot = await findBotByTrigger(botRepository, content, instance.instanceId); findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
if (!findBot) { if (!findBot) {
return; return null;
} }
} else { } else {
findBot = await botRepository.findFirst({ 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 { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
import { Router } from 'express'; import { Router } from 'express';
import { EvoaiRouter } from './evoai/routes/evoai.router';
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router'; import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
import { FlowiseRouter } from './flowise/routes/flowise.router'; import { FlowiseRouter } from './flowise/routes/flowise.router';
import { N8nRouter } from './n8n/routes/n8n.router';
export class ChatbotRouter { export class ChatbotRouter {
public readonly router: Router; public readonly router: Router;
@ -19,5 +21,7 @@ export class ChatbotRouter {
this.router.use('/openai', new OpenaiRouter(...guards).router); this.router.use('/openai', new OpenaiRouter(...guards).router);
this.router.use('/dify', new DifyRouter(...guards).router); this.router.use('/dify', new DifyRouter(...guards).router);
this.router.use('/flowise', new FlowiseRouter(...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/chatwoot/validate/chatwoot.schema';
export * from '@api/integrations/chatbot/dify/validate/dify.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/evolutionBot/validate/evolutionBot.schema';
export * from '@api/integrations/chatbot/flowise/validate/flowise.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/openai/validate/openai.schema';
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema'; export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';

View File

@ -295,51 +295,57 @@ export class ChatwootService {
avatar_url?: string, avatar_url?: string,
jid?: string, jid?: string,
) { ) {
const client = await this.clientCw(instance); try {
const client = await this.clientCw(instance);
if (!client) { if (!client) {
this.logger.warn('client not found'); this.logger.warn('client not found');
return null; 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}`;
} }
} else {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: phoneNumber,
avatar_url: avatar_url,
};
}
const contact = await client.contacts.create({ let data: any = {};
accountId: this.provider.accountId, if (!isGroup) {
data, data = {
}); inbox_id: inboxId,
name: name || phoneNumber,
identifier: jid,
avatar_url: avatar_url,
};
if (!contact) { if ((jid && jid.includes('@')) || !jid) {
this.logger.warn('contact not found'); 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; 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) { public async updateContact(instance: InstanceDto, id: number, data: any) {
@ -543,216 +549,240 @@ export class ChatwootService {
} }
public async createConversation(instance: InstanceDto, body: any) { 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 { 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)}`); this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
const client = await this.clientCw(instance); // If it already exists in the cache, return conversationId
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 (await this.cache.has(cacheKey)) { if (await this.cache.has(cacheKey)) {
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
const conversationId = (await this.cache.get(cacheKey)) as number; const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Cached conversation ID: ${conversationId}`); this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
let conversationExists: conversation | boolean;
try {
conversationExists = await client.conversations.get({
accountId: this.provider.accountId,
conversationId: conversationId,
});
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
} catch (error) {
this.logger.error(`Error getting conversation: ${error}`);
conversationExists = false;
}
if (!conversationExists) {
this.logger.verbose('Conversation does not exist, re-calling createConversation');
this.cache.delete(cacheKey);
return await this.createConversation(instance, body);
}
return conversationId; return conversationId;
} }
const isGroup = body.key.remoteJid.includes('@g.us'); // If lock already exists, wait until release or timeout
this.logger.verbose(`Is group: ${isGroup}`); if (await this.cache.has(lockKey)) {
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; const start = Date.now();
this.logger.verbose(`Chat ID: ${chatId}`); while (await this.cache.has(lockKey)) {
if (Date.now() - start > maxWaitTime) {
let nameContact: string; this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
break;
nameContact = !body.key.fromMe ? body.pushName : chatId; }
this.logger.verbose(`Name contact: ${nameContact}`); await new Promise((res) => setTimeout(res, 300));
if (await this.cache.has(cacheKey)) {
const filterInbox = await this.getInbox(instance); const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
if (!filterInbox) { return conversationId;
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`); }
return null; }
} }
if (isGroup) { // Adquire lock
this.logger.verbose('Processing group conversation'); await this.cache.set(lockKey, true, 30);
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
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( const client = await this.clientCw(instance);
body.key.participant.split('@')[0], if (!client) return null;
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); const isGroup = remoteJid.includes('@g.us');
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`); 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 (isGroup) {
if (!findParticipant.name || findParticipant.name === chatId) { this.logger.verbose(`Processing group conversation`);
await this.updateContact(instance, findParticipant.id, { const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
name: body.pushName, this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
avatar_url: picture_url.profilePictureUrl || null,
}); 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 { } else {
await this.createContact( const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact(
instance, instance,
body.key.participant.split('@')[0], chatId,
filterInbox.id, filterInbox.id,
false, isGroup,
body.pushName, nameContact,
picture_url.profilePictureUrl || null, picture_url.profilePictureUrl || null,
body.key.participant, jid,
); );
} }
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); if (!contact) {
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); this.logger.warn(`Contact not created or found`);
return null;
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 }),
});
}
} }
} else {
const jid = body.key.remoteJid; const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
contact = await this.createContact( this.logger.verbose(`Contact ID: ${contactId}`);
instance,
chatId, const contactConversations = (await client.contacts.listConversations({
filterInbox.id, accountId: this.provider.accountId,
isGroup, id: contactId,
nameContact, })) as any;
picture_url.profilePictureUrl || null, this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
jid,
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 (inboxConversation) {
if (this.provider.reopenConversation) {
if (!contact) { this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
this.logger.warn('Contact not created or found'); if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
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) {
await client.conversations.toggleStatus({ await client.conversations.toggleStatus({
accountId: this.provider.accountId, accountId: this.provider.accountId,
conversationId: conversation.id, conversationId: inboxConversation.id,
data: { data: {
status: 'pending', 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) { const data = {
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`); contact_id: contactId.toString(),
this.cache.set(cacheKey, conversation.id); inbox_id: filterInbox.id.toString(),
return conversation.id; };
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) { } catch (error) {
this.logger.error(`Error in createConversation: ${error}`); this.logger.error(`Error in createConversation: ${error}`);
return null;
} }
} }
@ -931,7 +961,7 @@ export class ChatwootService {
quotedMsg?: MessageModel, quotedMsg?: MessageModel,
) { ) {
if (sourceId && this.isImportHistoryAvailable()) { if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]); const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
if (messageAlreadySaved) { if (messageAlreadySaved) {
if (messageAlreadySaved.size > 0) { if (messageAlreadySaved.size > 0) {
this.logger.warn('Message already saved on chatwoot'); this.logger.warn('Message already saved on chatwoot');
@ -1106,12 +1136,13 @@ export class ChatwootService {
sendTelemetry('/message/sendWhatsAppAudio'); sendTelemetry('/message/sendWhatsAppAudio');
const messageSent = await waInstance?.audioWhatsapp(data, true); const messageSent = await waInstance?.audioWhatsapp(data, null, true);
return messageSent; 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'; type = 'document';
} }
@ -1653,7 +1684,7 @@ export class ChatwootService {
stickerMessage: undefined, stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption, documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage?.caption, audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard, contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage, contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage, locationMessage: msg.locationMessage,
@ -1898,7 +1929,7 @@ export class ChatwootService {
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~') .replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage; : originalMessage;
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) { if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
return; return;
} }
@ -2199,7 +2230,7 @@ export class ChatwootService {
} }
} }
if (event === 'messages.edit') { if (event === 'messages.edit' || event === 'send.message.update') {
const editedText = `${ const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`; }\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 { try {
const existingSourceIdsSet = new Set<string>(); const existingSourceIdsSet = new Set<string>();
@ -177,18 +177,25 @@ class ChatwootImport {
return existingSourceIdsSet; return existingSourceIdsSet;
} }
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890 // Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)'; const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
const pgClient = postgresClient.getChatwootConnection(); 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) { for (const row of result.rows) {
existingSourceIdsSet.add(row.source_id); existingSourceIdsSet.add(row.source_id);
} }
return existingSourceIdsSet; return existingSourceIdsSet;
} catch (error) { } 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, stickerMessage: msg.message.stickerMessage,
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText, 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) { switch (typeKey) {
case 'documentMessage': case 'documentMessage': {
return `_<File: ${msg.message.documentMessage.fileName}${ const doc = msg.message.documentMessage;
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : '' const fileName = doc?.fileName || 'document';
}>_`; const caption = doc?.caption ? ` ${doc.caption}` : '';
return `_<File: ${fileName}${caption}>_`;
}
case 'documentWithCaptionMessage': case 'documentWithCaptionMessage': {
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${ const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
msg.message.documentWithCaptionMessage.message.documentMessage.caption const fileName = doc?.fileName || 'document';
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}` const caption = doc?.caption ? ` ${doc.caption}` : '';
: '' return `_<File: ${fileName}${caption}>_`;
}>_`; }
case 'templateMessage': case 'templateMessage': {
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText const template = msg.message.templateMessage?.hydratedTemplate;
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n` return (
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText; (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
(template?.hydratedContentText || '')
);
}
case 'imageMessage': case 'imageMessage':
return '_<Image Message>_'; return '_<Image Message>_';

View File

@ -1,4 +1,3 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto'; import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service'; 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 { configService, Dify } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { Dify as DifyModel } from '@prisma/client'; import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
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( constructor(
private readonly difyService: DifyService, private readonly difyService: DifyService,
prismaRepository: PrismaRepository, prismaRepository: PrismaRepository,
@ -26,6 +24,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
} }
public readonly logger = new Logger('DifyController'); public readonly logger = new Logger('DifyController');
protected readonly integrationName = 'Dify';
integrationEnabled = configService.get<Dify>('DIFY').ENABLED; integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
botRepository: any; botRepository: any;
@ -33,261 +32,37 @@ export class DifyController extends ChatbotController implements ChatbotControll
sessionRepository: any; sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots protected getFallbackBotId(settings: any): string | undefined {
public async createBot(instance: InstanceDto, data: DifyDto) { return settings?.fallbackId;
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');
}
} }
public async findBot(instance: InstanceDto) { protected getFallbackFieldName(): string {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled'); return 'difyIdFallback';
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) { protected getIntegrationType(): string {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled'); return 'dify';
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;
} }
public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) { protected getAdditionalBotData(data: DifyDto): Record<string, any> {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled'); return {
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
const instanceId = await this.prismaRepository.instance // Implementation for bot-specific updates
.findFirst({ protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
where: { return {
name: instance.instanceName, botType: data.botType,
}, apiUrl: data.apiUrl,
}) apiKey: data.apiKey,
.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 duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({ const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
id: { id: {
@ -303,81 +78,10 @@ export class DifyController extends ChatbotController implements ChatbotControll
if (checkDuplicate) { if (checkDuplicate) {
throw new Error('Dify already exists'); 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'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance const instanceId = await this.prismaRepository.instance
@ -388,501 +92,35 @@ export class DifyController extends ChatbotController implements ChatbotControll
}) })
.then((instance) => instance.id); .then((instance) => instance.id);
const bot = await this.botRepository.findFirst({ // Dify-specific duplicate check
const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
id: botId, instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
}, },
}); });
if (!bot) { if (checkDuplicate) {
throw new Error('Dify not found'); throw new Error('Dify already exists');
} }
if (bot.instanceId !== instanceId) { // Let the base class handle the rest
throw new Error('Dify not found'); return super.createBot(instance, data);
}
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');
}
} }
// Settings // Process Dify-specific bot logic
public async settings(instance: InstanceDto, data: any) { protected async processBot(
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled'); instance: any,
remoteJid: string,
try { bot: DifyModel,
const instanceId = await this.prismaRepository.instance session: IntegrationSession,
.findFirst({ settings: any,
where: { content: string,
name: instance.instanceName, pushName?: string,
}, msg?: any,
}) ) {
.then((instance) => instance.id); await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
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;
}
} }
} }

View File

@ -1,38 +1,13 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; import { $Enums } from '@prisma/client';
export class DifyDto { import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
enabled?: boolean;
description?: string; export class DifyDto extends BaseChatbotDto {
botType?: $Enums.DifyBotType; botType?: $Enums.DifyBotType;
apiUrl?: string; apiUrl?: string;
apiKey?: 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 { export class DifySettingDto extends BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
difyIdFallback?: string; 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 { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types'; import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config'; import { ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Dify, DifySetting, IntegrationSession } from '@prisma/client'; import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios'; 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( constructor(
private readonly waMonitor: WAMonitoringService, waMonitor: WAMonitoringService,
private readonly configService: ConfigService, prismaRepository: PrismaRepository,
private readonly prismaRepository: PrismaRepository, configService: ConfigService,
) {} openaiService: OpenaiService,
) {
private readonly logger = new Logger('DifyService'); super(waMonitor, prismaRepository, 'DifyService', configService);
this.openaiService = openaiService;
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;
}
} }
private isImageMessage(content: string) { /**
return content.includes('imageMessage'); * Return the bot type for Dify
*/
protected getBotType(): string {
return 'dify';
} }
private isJSON(str: string): boolean { protected async sendMessageToBot(
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
private async sendMessageToBot(
instance: any, instance: any,
session: IntegrationSession, session: IntegrationSession,
settings: DifySetting, settings: DifySetting,
@ -62,10 +36,30 @@ export class DifyService {
remoteJid: string, remoteJid: string,
pushName: string, pushName: string,
content: string, content: string,
) { msg?: any,
): Promise<void> {
try { try {
let endpoint: string = dify.apiUrl; 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') { if (dify.botType === 'chatBot') {
endpoint += '/chat-messages'; endpoint += '/chat-messages';
const payload: any = { const payload: any = {
@ -74,17 +68,17 @@ export class DifyService {
pushName: pushName, pushName: pushName,
instanceName: instance.instanceName, instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL, 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', response_mode: 'blocking',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid, user: remoteJid,
}; };
// Handle image messages
if (this.isImageMessage(content)) { if (this.isImageMessage(content)) {
const contentSplit = content.split('|'); const contentSplit = content.split('|');
payload.files = [ payload.files = [
{ {
type: 'image', type: 'image',
@ -112,7 +106,9 @@ export class DifyService {
const message = response?.data?.answer; const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id; 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({ await this.prismaRepository.integrationSession.update({
where: { where: {
@ -130,21 +126,21 @@ export class DifyService {
endpoint += '/completion-messages'; endpoint += '/completion-messages';
const payload: any = { const payload: any = {
inputs: { inputs: {
query: content, query: processedContent,
pushName: pushName, pushName: pushName,
remoteJid: remoteJid, remoteJid: remoteJid,
instanceName: instance.instanceName, instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL, serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY, apiKey: instance.token,
}, },
response_mode: 'blocking', response_mode: 'blocking',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid, user: remoteJid,
}; };
// Handle image messages
if (this.isImageMessage(content)) { if (this.isImageMessage(content)) {
const contentSplit = content.split('|'); const contentSplit = content.split('|');
payload.files = [ payload.files = [
{ {
type: 'image', type: 'image',
@ -172,7 +168,9 @@ export class DifyService {
const message = response?.data?.answer; const message = response?.data?.answer;
const conversationId = response?.data?.conversation_id; 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({ await this.prismaRepository.integrationSession.update({
where: { where: {
@ -194,17 +192,17 @@ export class DifyService {
pushName: pushName, pushName: pushName,
instanceName: instance.instanceName, instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL, 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', response_mode: 'streaming',
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid, user: remoteJid,
}; };
// Handle image messages
if (this.isImageMessage(content)) { if (this.isImageMessage(content)) {
const contentSplit = content.split('|'); const contentSplit = content.split('|');
payload.files = [ payload.files = [
{ {
type: 'image', type: 'image',
@ -230,7 +228,6 @@ export class DifyService {
let answer = ''; let answer = '';
const data = response.data.replaceAll('data: ', ''); const data = response.data.replaceAll('data: ', '');
const events = data.split('\n').filter((line) => line.trim() !== ''); const events = data.split('\n').filter((line) => line.trim() !== '');
for (const eventString of events) { for (const eventString of events) {
@ -248,9 +245,9 @@ export class DifyService {
if (instance.integration === Integration.WHATSAPP_BAILEYS) if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid); await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = answer; if (answer) {
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings); }
await this.prismaRepository.integrationSession.update({ await this.prismaRepository.integrationSession.update({
where: { where: {
@ -259,369 +256,13 @@ export class DifyService {
data: { data: {
status: 'opened', status: 'opened',
awaitUser: true, 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) { } catch (error) {
this.logger.error(error.response?.data || error); this.logger.error(error.response?.data || error);
return; 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 { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { EvolutionBot } from '@prisma/client'; import { EvolutionBot, IntegrationSession } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; import { BaseChatbotController } from '../../base-chatbot.controller';
import { EvolutionBotDto } from '../dto/evolutionBot.dto'; import { EvolutionBotDto } from '../dto/evolutionBot.dto';
import { EvolutionBotService } from '../services/evolutionBot.service'; import { EvolutionBotService } from '../services/evolutionBot.service';
export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface { export class EvolutionBotController extends BaseChatbotController<EvolutionBot, EvolutionBotDto> {
constructor( constructor(
private readonly evolutionBotService: EvolutionBotService, private readonly evolutionBotService: EvolutionBotService,
prismaRepository: PrismaRepository, prismaRepository: PrismaRepository,
@ -24,258 +21,49 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
} }
public readonly logger = new Logger('EvolutionBotController'); 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; botRepository: any;
settingsRepository: any; settingsRepository: any;
sessionRepository: any; sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots // Implementation of abstract methods required by BaseChatbotController
public async createBot(instance: InstanceDto, data: EvolutionBotDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if ( protected getFallbackBotId(settings: any): string | undefined {
!data.expire || return settings?.botIdFallback;
!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');
}
} }
public async findBot(instance: InstanceDto) { protected getFallbackFieldName(): string {
const instanceId = await this.prismaRepository.instance return 'botIdFallback';
.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) { protected getIntegrationType(): string {
const instanceId = await this.prismaRepository.instance return 'evolution';
.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: EvolutionBotDto) { protected getAdditionalBotData(data: EvolutionBotDto): Record<string, any> {
const instanceId = await this.prismaRepository.instance return {
.findFirst({ apiUrl: data.apiUrl,
where: { apiKey: data.apiKey,
name: instance.instanceName, };
}, }
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({ // Implementation for bot-specific updates
where: { protected getAdditionalUpdateFields(data: EvolutionBotDto): Record<string, any> {
id: botId, return {
}, apiUrl: data.apiUrl,
}); apiKey: data.apiKey,
};
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 duplicate validation on update
protected async validateNoDuplicatesOnUpdate(
botId: string,
instanceId: string,
data: EvolutionBotDto,
): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({ const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
id: { id: {
@ -288,573 +76,21 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
}); });
if (checkDuplicate) { if (checkDuplicate) {
throw new Error('Bot already exists'); throw new Error('Evolution 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) { // Process bot-specific logic
const instanceId = await this.prismaRepository.instance protected async processBot(
.findFirst({ instance: any,
where: { remoteJid: string,
name: instance.instanceName, bot: EvolutionBot,
}, session: IntegrationSession,
}) settings: any,
.then((instance) => instance.id); content: string,
pushName?: string,
const bot = await this.botRepository.findFirst({ msg?: any,
where: { ) {
id: botId, await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
},
});
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;
}
} }
} }

View File

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

View File

@ -1,428 +1,138 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types'; import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config'; import { ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client'; import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry'; import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios'; import axios from 'axios';
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( constructor(
private readonly waMonitor: WAMonitoringService, waMonitor: WAMonitoringService,
private readonly configService: ConfigService, prismaRepository: PrismaRepository,
private readonly prismaRepository: PrismaRepository, configService: ConfigService,
) {} openaiService: OpenaiService,
) {
private readonly logger = new Logger('EvolutionBotService'); super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
this.openaiService = openaiService;
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;
}
} }
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, instance: any,
session: IntegrationSession, session: IntegrationSession,
settings: EvolutionBotSetting,
bot: EvolutionBot, bot: EvolutionBot,
remoteJid: string, remoteJid: string,
pushName: string, pushName: string,
content: string, content: string,
) { msg?: any,
const payload: any = { ): Promise<void> {
inputs: { try {
sessionId: session.id, const payload: any = {
remoteJid: remoteJid, inputs: {
pushName: pushName, sessionId: session.id,
instanceName: instance.instanceName, remoteJid: remoteJid,
serverUrl: this.configService.get<HttpServer>('SERVER').URL, pushName: pushName,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY, fromMe: msg?.key?.fromMe,
}, instanceName: instance.instanceName,
query: content, serverUrl: this.configService.get<HttpServer>('SERVER').URL,
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, apiKey: instance.token,
user: remoteJid,
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
url: contentSplit[1].split('?')[0],
}, },
]; query: content,
payload.query = contentSplit[2] || content; conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
} user: remoteJid,
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}`,
}; };
}
const response = await axios.post(bot.apiUrl, payload, { if (this.isAudioMessage(content) && msg) {
headers, try {
}); this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (instance.integration === Integration.WHATSAPP_BAILEYS) if (transcription) {
await instance.client.sendPresenceUpdate('paused', remoteJid); payload.query = `[audio] ${transcription}`;
}
const message = response?.data?.message; } catch (err) {
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
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 (mediaType) { if (this.isImageMessage(content)) {
const splitMessages = settings.splitMessages ?? false; const contentSplit = content.split('|');
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) { payload.files = [
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], type: 'image',
delay: settings?.delayMessage || 1000, url: contentSplit[1].split('?')[0],
text: textBuffer.trim(),
}, },
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({ const endpoint = bot.apiUrl;
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
}
private async initNewSession( if (!endpoint) {
instance: any, this.logger.error('No Evolution Bot endpoint defined');
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);
return; return;
} }
}
if (!session) { let headers: any = {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName); 'Content-Type': 'application/json',
return; };
}
await this.prismaRepository.integrationSession.update({ if (bot.apiKey) {
where: { headers = {
id: session.id, ...headers,
}, Authorization: `Bearer ${bot.apiKey}`,
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()) { const response = await axios.post(endpoint, payload, {
if (settings.keepOpen) { headers,
await this.prismaRepository.integrationSession.update({ });
where: {
id: session.id, if (instance.integration === Integration.WHATSAPP_BAILEYS) {
}, await instance.client.sendPresenceUpdate('paused', remoteJid);
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: 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; 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 { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Flowise } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { Flowise } from '@prisma/client'; import { BadRequestException } from '@exceptions';
import { getConversationMessage } from '@utils/getConversationMessage'; 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 { FlowiseDto } from '../dto/flowise.dto';
import { FlowiseService } from '../services/flowise.service'; import { FlowiseService } from '../services/flowise.service';
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface { export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
constructor( constructor(
private readonly flowiseService: FlowiseService, private readonly flowiseService: FlowiseService,
prismaRepository: PrismaRepository, prismaRepository: PrismaRepository,
@ -24,15 +24,73 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
} }
public readonly logger = new Logger('FlowiseController'); public readonly logger = new Logger('FlowiseController');
protected readonly integrationName = 'Flowise';
integrationEnabled: boolean; integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
botRepository: any; botRepository: any;
settingsRepository: any; settingsRepository: any;
sessionRepository: any; sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; 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) { public async createBot(instance: InstanceDto, data: FlowiseDto) {
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
const instanceId = await this.prismaRepository.instance const instanceId = await this.prismaRepository.instance
.findFirst({ .findFirst({
where: { where: {
@ -41,74 +99,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
}) })
.then((instance) => instance.id); .then((instance) => instance.id);
if ( // Flowise-specific duplicate check
!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');
}
const checkDuplicate = await this.botRepository.findFirst({ const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
instanceId: instanceId, instanceId: instanceId,
@ -121,740 +112,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
throw new Error('Flowise already exists'); throw new Error('Flowise already exists');
} }
if (data.triggerType === 'keyword') { // Let the base class handle the rest
if (!data.triggerOperator || !data.triggerValue) { return super.createBot(instance, data);
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;
}
} }
} }

View File

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

View File

@ -1,50 +1,57 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types'; import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config'; import { ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios'; 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( constructor(
private readonly waMonitor: WAMonitoringService, waMonitor: WAMonitoringService,
private readonly configService: ConfigService, prismaRepository: PrismaRepository,
private readonly prismaRepository: PrismaRepository, configService: ConfigService,
) {} openaiService: OpenaiService,
) {
private readonly logger = new Logger('FlowiseService'); super(waMonitor, prismaRepository, 'FlowiseService', configService);
this.openaiService = openaiService;
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;
}
} }
private isImageMessage(content: string) { // Return the bot type for Flowise
return content.includes('imageMessage'); 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 = { const payload: any = {
question: content, question: content,
overrideConfig: { overrideConfig: {
@ -54,11 +61,24 @@ export class FlowiseService {
pushName: pushName, pushName: pushName,
instanceName: instance.instanceName, instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL, 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)) { if (this.isImageMessage(content)) {
const contentSplit = content.split('|'); const contentSplit = content.split('|');
@ -91,335 +111,26 @@ export class FlowiseService {
const endpoint = bot.apiUrl; 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, { const response = await axios.post(endpoint, payload, {
headers, headers,
}); });
if (instance.integration === Integration.WHATSAPP_BAILEYS) if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid); await instance.client.sendPresenceUpdate('paused', remoteJid);
}
const message = response?.data?.text; 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( // The service is now complete with just the abstract method implementations
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;
}
} }

View File

@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' }, keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' }, debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } }, ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
}, },
required: ['enabled', 'apiUrl', 'triggerType'], required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'), ...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
keepOpen: { type: 'boolean' }, keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' }, debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } }, ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' }, flowiseIdFallback: { type: 'string' },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'integer' },
}, },
required: [ required: [
'expire', '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 { export class OpenaiCredsDto {
name: string; name: string;
apiKey: string; apiKey: string;
} }
export class OpenaiDto { export class OpenaiDto extends BaseChatbotDto {
enabled?: boolean;
description?: string;
openaiCredsId: string; openaiCredsId: string;
botType?: string; botType: string;
assistantId?: string; assistantId?: string;
functionUrl?: string; functionUrl?: string;
model?: string; model?: string;
@ -17,35 +15,10 @@ export class OpenaiDto {
assistantMessages?: string[]; assistantMessages?: string[];
userMessages?: string[]; userMessages?: string[];
maxTokens?: number; 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; openaiCredsId?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
openaiIdFallback?: string; openaiIdFallback?: string;
ignoreJids?: any;
speechToText?: boolean; speechToText?: boolean;
splitMessages?: boolean;
timePerChar?: number;
} }

View File

@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => openaiController.getModels(instance), execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
}); });
res.status(HttpStatus.OK).json(response); 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 { InstanceDto } from '@api/dto/instance.dto';
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service'; 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 { configService, Typebot } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { Typebot as TypebotModel } from '@prisma/client'; import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import axios from 'axios'; 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( constructor(
private readonly typebotService: TypebotService, private readonly typebotService: TypebotService,
prismaRepository: PrismaRepository, prismaRepository: PrismaRepository,
@ -28,6 +26,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
} }
public readonly logger = new Logger('TypebotController'); public readonly logger = new Logger('TypebotController');
protected readonly integrationName = 'Typebot';
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED; integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
botRepository: any; botRepository: any;
@ -35,245 +34,35 @@ export class TypebotController extends ChatbotController implements ChatbotContr
sessionRepository: any; sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots protected getFallbackBotId(settings: any): string | undefined {
public async createBot(instance: InstanceDto, data: TypebotDto) { return settings?.typebotIdFallback;
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');
}
} }
public async findBot(instance: InstanceDto) { protected getFallbackFieldName(): string {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); return 'typebotIdFallback';
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) { protected getIntegrationType(): string {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); return 'typebot';
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;
} }
public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) { protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); return {
url: data.url,
typebot: data.typebot,
};
}
const instanceId = await this.prismaRepository.instance // Implementation for bot-specific updates
.findFirst({ protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
where: { return {
name: instance.instanceName, url: data.url,
}, typebot: data.typebot,
}) };
.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 duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({ const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
url: data.url, url: data.url,
@ -288,263 +77,41 @@ export class TypebotController extends ChatbotController implements ChatbotContr
if (checkDuplicate) { if (checkDuplicate) {
throw new Error('Typebot already exists'); 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) { // Process Typebot-specific bot logic
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); protected async processBot(
instance: any,
const instanceId = await this.prismaRepository.instance remoteJid: string,
.findFirst({ bot: TypebotModel,
where: { session: IntegrationSession,
name: instance.instanceName, settings: any,
}, content: string,
}) pushName?: string,
.then((instance) => instance.id); msg?: any,
) {
const typebot = await this.botRepository.findFirst({ // Map to the original processTypebot method signature
where: { await this.typebotService.processTypebot(
id: botId, instance,
}, remoteJid,
}); msg,
session,
if (!typebot) { bot,
throw new Error('Typebot not found'); bot.url,
} settings.expire,
bot.typebot,
if (typebot.instanceId !== instanceId) { settings.keywordFinish,
throw new Error('Typebot not found'); settings.delayMessage,
} settings.unknownMessage,
try { settings.listeningFromMe,
await this.prismaRepository.integrationSession.deleteMany({ settings.stopBotFromMe,
where: { settings.keepOpen,
botId: botId, content,
}, {}, // prefilledVariables (optional)
}); );
await this.botRepository.delete({
where: {
id: botId,
},
});
return { typebot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting typebot');
}
} }
// Settings // TypeBot specific method for starting a bot from API
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
public async startBot(instance: InstanceDto, data: any) { public async startBot(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); 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({ const instanceData = await this.prismaRepository.instance.findFirst({
where: { 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( await this.typebotService.processTypebot(
instanceData, this.waMonitor.waInstances[instanceData.name],
remoteJid, remoteJid,
null, null, // msg
null, null, // session
findBot, findBot,
url, url,
expire, expire,
@ -722,7 +290,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
request.data.clientSideActions, request.data.clientSideActions,
); );
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
remoteJid: remoteJid, remoteJid: remoteJid,
url: url, url: url,
typebot: typebot, 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 { export class PrefilledVariables {
remoteJid?: string; remoteJid?: string;
@ -7,34 +7,11 @@ export class PrefilledVariables {
additionalData?: { [key: string]: any }; additionalData?: { [key: string]: any };
} }
export class TypebotDto { export class TypebotDto extends BaseChatbotDto {
enabled?: boolean;
description?: string;
url: string; url: string;
typebot?: 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;
} }
export class TypebotSettingDto { export class TypebotSettingDto extends BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
typebotIdFallback?: string; typebotIdFallback?: string;
ignoreJids?: any;
} }

View File

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

View File

@ -26,6 +26,11 @@ export class EventDto {
events?: string[]; events?: string[];
}; };
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: { pusher?: {
enabled?: boolean; enabled?: boolean;
appId?: string; appId?: string;
@ -63,6 +68,11 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
events?: string[]; events?: string[];
}; };
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: { pusher?: {
enabled?: boolean; enabled?: boolean;
appId?: string; 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 { PusherController } from '@api/integrations/event/pusher/pusher.controller';
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller'; import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
import { SqsController } from '@api/integrations/event/sqs/sqs.controller'; import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
@ -13,6 +14,7 @@ export class EventManager {
private websocketController: WebsocketController; private websocketController: WebsocketController;
private webhookController: WebhookController; private webhookController: WebhookController;
private rabbitmqController: RabbitmqController; private rabbitmqController: RabbitmqController;
private natsController: NatsController;
private sqsController: SqsController; private sqsController: SqsController;
private pusherController: PusherController; private pusherController: PusherController;
@ -23,6 +25,7 @@ export class EventManager {
this.websocket = new WebsocketController(prismaRepository, waMonitor); this.websocket = new WebsocketController(prismaRepository, waMonitor);
this.webhook = new WebhookController(prismaRepository, waMonitor); this.webhook = new WebhookController(prismaRepository, waMonitor);
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor); this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
this.nats = new NatsController(prismaRepository, waMonitor);
this.sqs = new SqsController(prismaRepository, waMonitor); this.sqs = new SqsController(prismaRepository, waMonitor);
this.pusher = new PusherController(prismaRepository, waMonitor); this.pusher = new PusherController(prismaRepository, waMonitor);
} }
@ -67,6 +70,14 @@ export class EventManager {
return this.rabbitmqController; return this.rabbitmqController;
} }
public set nats(nats: NatsController) {
this.natsController = nats;
}
public get nats() {
return this.natsController;
}
public set sqs(sqs: SqsController) { public set sqs(sqs: SqsController) {
this.sqsController = sqs; this.sqsController = sqs;
} }
@ -85,6 +96,7 @@ export class EventManager {
public init(httpServer: Server): void { public init(httpServer: Server): void {
this.websocket.init(httpServer); this.websocket.init(httpServer);
this.rabbitmq.init(); this.rabbitmq.init();
this.nats.init();
this.sqs.init(); this.sqs.init();
this.pusher.init(); this.pusher.init();
} }
@ -103,6 +115,7 @@ export class EventManager {
}): Promise<void> { }): Promise<void> {
await this.websocket.emit(eventData); await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData); await this.rabbitmq.emit(eventData);
await this.nats.emit(eventData);
await this.sqs.emit(eventData); await this.sqs.emit(eventData);
await this.webhook.emit(eventData); await this.webhook.emit(eventData);
await this.pusher.emit(eventData); await this.pusher.emit(eventData);
@ -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) if (data.sqs)
await this.sqs.set(instanceName, { await this.sqs.set(instanceName, {
sqs: { 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 { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router'; import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
import { SqsRouter } from '@api/integrations/event/sqs/sqs.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('/webhook', new WebhookRouter(configService, ...guards).router);
this.router.use('/websocket', new WebsocketRouter(...guards).router); this.router.use('/websocket', new WebsocketRouter(...guards).router);
this.router.use('/rabbitmq', new RabbitmqRouter(...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('/pusher', new PusherRouter(...guards).router);
this.router.use('/sqs', new SqsRouter(...guards).router); this.router.use('/sqs', new SqsRouter(...guards).router);
} }

View File

@ -16,6 +16,9 @@ export const eventSchema: JSONSchema7 = {
rabbitmq: { rabbitmq: {
$ref: '#/$defs/event', $ref: '#/$defs/event',
}, },
nats: {
$ref: '#/$defs/event',
},
sqs: { sqs: {
$ref: '#/$defs/event', $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) => { await new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI; const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME; 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) { if (error) {
reject(error); reject(error);

View File

@ -1,10 +1,11 @@
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.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 { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { EmitData, EventController, EventControllerInterface } from '../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../event.controller';
import { EventDto } from '../event.dto';
export class SqsController extends EventController implements EventControllerInterface { export class SqsController extends EventController implements EventControllerInterface {
private sqs: SQS; private sqs: SQS;
@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt
return this.sqs; 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({ public async emit({
instanceName, instanceName,
origin, origin,
@ -121,70 +155,92 @@ export class SqsController extends EventController implements EventControllerInt
} }
} }
public async initQueues(instanceName: string, events: string[]) { private async saveQueues(instanceName: string, events: string[], enable: boolean) {
if (!events || !events.length) return; if (enable) {
const eventsFinded = await this.listQueuesByInstance(instanceName);
console.log('eventsFinded', eventsFinded);
const queues = events.map((event) => { for (const event of events) {
return `${event.replace(/_/g, '_').toLowerCase()}`; const normalizedEvent = event.toLowerCase();
});
queues.forEach((event) => { if (eventsFinded.includes(normalizedEvent)) {
const queueName = `${instanceName}_${event}.fifo`; this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
continue;
}
this.sqs.createQueue( const queueName = `${instanceName}_${normalizedEvent}.fifo`;
{
QueueName: queueName, try {
Attributes: { const createCommand = new CreateQueueCommand({
FifoQueue: 'true', QueueName: queueName,
}, Attributes: {
}, FifoQueue: 'true',
(err, data) => { },
if (err) { });
this.logger.error(`Error creating queue ${queueName}: ${err.message}`); const data = await this.sqs.send(createCommand);
} else { this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`); } catch (err: any) {
} this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
}, }
); }
}); }
} }
public async removeQueues(instanceName: string, events: any) { private async listQueuesByInstance(instanceName: string) {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : []; let existingQueues: string[] = [];
if (!events || !eventsArray.length) return; 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) => { // Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
return `${event.replace(/_/g, '_').toLowerCase()}`; 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) => { // Para uma futura feature de exclusão forçada das queues
const queueName = `${instanceName}_${event}.fifo`; private async removeQueuesByInstance(instanceName: string) {
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
this.sqs.getQueueUrl( if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
{ this.logger.info(`No queues found for instance ${instanceName}`);
QueueName: queueName, return;
}, }
(err, data) => {
if (err) {
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
} else {
const queueUrl = data.QueueUrl;
this.sqs.deleteQueue( for (const queueUrl of listData.QueueUrls) {
{ try {
QueueUrl: queueUrl, const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
}, await this.sqs.send(deleteCommand);
(deleteErr) => { this.logger.info(`Queue ${queueUrl} deleted`);
if (deleteErr) { } catch (err: any) {
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`); this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
} else { }
this.logger.info(`Queue ${queueName} deleted`); }
} } 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 { wa } from '@api/types/wa.types';
import { configService, Log, Webhook } from '@config/env.config'; import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions'; // import { BadRequestException } from '@exceptions';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator'; import * as jwt from 'jsonwebtoken';
import { EmitData, EventController, EventControllerInterface } from '../event.controller'; 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> { override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
if (!isURL(data.webhook.url, { require_tld: false })) { // if (!/^(https?:\/\/)/.test(data.webhook.url)) {
throw new BadRequestException('Invalid "url" property'); // throw new BadRequestException('Invalid "url" property');
} // }
if (!data.webhook?.enabled) { if (!data.webhook?.enabled) {
data.webhook.events = []; data.webhook.events = [];
@ -74,10 +74,20 @@ export class WebhookController extends EventController implements EventControlle
const webhookConfig = configService.get<Webhook>('WEBHOOK'); const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events; 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 we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS'); const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const regex = /^(https?:\/\/)/;
const webhookData = { const webhookData = {
event, event,
@ -111,10 +121,11 @@ export class WebhookController extends EventController implements EventControlle
} }
try { try {
if (instance?.enabled && isURL(instance.url, { require_tld: false })) { if (instance?.enabled && regex.test(instance.url)) {
const httpService = axios.create({ const httpService = axios.create({
baseURL, baseURL,
headers: webhookHeaders as Record<string, string> | undefined, headers: webhookHeaders as Record<string, string> | undefined,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
}); });
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl); await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
@ -155,8 +166,11 @@ export class WebhookController extends EventController implements EventControlle
} }
try { try {
if (isURL(globalURL)) { if (regex.test(globalURL)) {
const httpService = axios.create({ baseURL: globalURL }); const httpService = axios.create({
baseURL: globalURL,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
await this.retryWebhookRequest( await this.retryWebhookRequest(
httpService, httpService,
@ -190,12 +204,20 @@ export class WebhookController extends EventController implements EventControlle
origin: string, origin: string,
baseURL: string, baseURL: string,
serverUrl: string, serverUrl: string,
maxRetries = 10, maxRetries?: number,
delaySeconds = 30, delaySeconds?: number,
): Promise<void> { ): 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; let attempts = 0;
while (attempts < maxRetries) { while (attempts < maxRetryAttempts) {
try { try {
await httpService.post('', webhookData); await httpService.post('', webhookData);
if (attempts > 0) { if (attempts > 0) {
@ -209,12 +231,27 @@ export class WebhookController extends EventController implements EventControlle
} catch (error) { } catch (error) {
attempts++; 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({ this.logger.error({
local: `${origin}`, local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`, message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,
code: error?.code, code: error?.code,
isTimeout,
statusCode: error?.response?.status,
error: error?.errno, error: error?.errno,
stack: error?.stack, stack: error?.stack,
name: error?.name, name: error?.name,
@ -222,12 +259,46 @@ export class WebhookController extends EventController implements EventControlle
server_url: serverUrl, server_url: serverUrl,
}); });
if (attempts === maxRetries) { if (attempts === maxRetryAttempts) {
throw error; 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 { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.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 { Logger } from '@config/logger.config';
import { Server } from 'http'; import { Server } from 'http';
import { Server as SocketIO } from 'socket.io'; import { Server as SocketIO } from 'socket.io';
@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl
} }
this.socket = new SocketIO(httpServer, { this.socket = new SocketIO(httpServer, {
cors: { cors: { origin: this.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); this.socket.emit(event, message);
if (logEnabled) { if (logEnabled) {
this.logger.log({ this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
local: `${origin}.sendData-WebsocketGlobal`,
...message,
});
} }
} }
@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.of(`/${instanceName}`).emit(event, message); this.socket.of(`/${instanceName}`).emit(event, message);
if (logEnabled) { if (logEnabled) {
this.logger.log({ this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
local: `${origin}.sendData-Websocket`,
...message,
});
} }
} }
} catch (err) { } catch (err) {

View File

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

View File

@ -1,7 +1,7 @@
import { Auth, ConfigService, ProviderSession } from '@config/env.config'; import { Auth, ConfigService, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import axios from 'axios'; import axios from 'axios';
import { execSync } from 'child_process'; import { execFileSync } from 'child_process';
type ResponseSuccess = { status: number; data?: any }; type ResponseSuccess = { status: number; data?: any };
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>; type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
@ -36,7 +36,7 @@ export class ProviderFiles {
} catch (error) { } catch (error) {
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]); this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
const pid = process.pid; 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(); super();
this.router this.router
.post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => {
const response = await this.dataValidate<WhatsAppNumberDto>({ try {
request: req, const response = await this.dataValidate<WhatsAppNumberDto>({
schema: whatsappNumberSchema, request: req,
ClassRef: WhatsAppNumberDto, schema: whatsappNumberSchema,
execute: (instance, data) => chatController.whatsappNumber(instance, data), 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) => { .post(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => {
const response = await this.dataValidate<ReadMessageDto>({ const response = await this.dataValidate<ReadMessageDto>({
@ -207,7 +212,6 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
}) })
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileNameDto>({ const response = await this.dataValidate<ProfileNameDto>({
request: req, 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 { EventRouter } from '@api/integrations/event/event.router';
import { StorageRouter } from '@api/integrations/storage/storage.router'; import { StorageRouter } from '@api/integrations/storage/storage.router';
import { configService } from '@config/env.config'; import { configService } from '@config/env.config';
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
import { Router } from 'express'; import { Router } from 'express';
import fs from 'fs'; import fs from 'fs';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; import path from 'path';
import { BusinessRouter } from './business.router';
import { CallRouter } from './call.router'; import { CallRouter } from './call.router';
import { ChatRouter } from './chat.router'; import { ChatRouter } from './chat.router';
import { GroupRouter } from './group.router'; import { GroupRouter } from './group.router';
@ -59,7 +61,7 @@ router.get('/assets/*', (req, res) => {
router router
.use((req, res, next) => telemetry.collectTelemetry(req, res, next)) .use((req, res, next) => telemetry.collectTelemetry(req, res, next))
.get('/', (req, res) => { .get('/', async (req, res) => {
res.status(HttpStatus.OK).json({ res.status(HttpStatus.OK).json({
status: HttpStatus.OK, status: HttpStatus.OK,
message: 'Welcome to the Evolution API, it is working!', message: 'Welcome to the Evolution API, it is working!',
@ -67,6 +69,8 @@ router
clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME, clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME,
manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined, manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined,
documentation: `https://doc.evolution-api.com`, 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) => { .post('/verify-creds', authGuard['apikey'], async (req, res) => {
@ -82,6 +86,7 @@ router
.use('/message', new MessageRouter(...guards).router) .use('/message', new MessageRouter(...guards).router)
.use('/call', new CallRouter(...guards).router) .use('/call', new CallRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router)
.use('/business', new BusinessRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router) .use('/group', new GroupRouter(...guards).router)
.use('/template', new TemplateRouter(configService, ...guards).router) .use('/template', new TemplateRouter(configService, ...guards).router)
.use('/settings', new SettingsRouter(...guards).router) .use('/settings', new SettingsRouter(...guards).router)

View File

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

View File

@ -3,6 +3,7 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
import { eventEmitter } from '@config/event.config'; import { eventEmitter } from '@config/event.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BusinessController } from './controllers/business.controller';
import { CallController } from './controllers/call.controller'; import { CallController } from './controllers/call.controller';
import { ChatController } from './controllers/chat.controller'; import { ChatController } from './controllers/chat.controller';
import { GroupController } from './controllers/group.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 { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller'; import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
import { DifyService } from './integrations/chatbot/dify/services/dify.service'; 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 { EvolutionBotController } from './integrations/chatbot/evolutionBot/controllers/evolutionBot.controller';
import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service'; import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service';
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller'; import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service'; 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 { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service'; import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller'; 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 sendMessageController = new SendMessageController(waMonitor);
export const callController = new CallController(waMonitor); export const callController = new CallController(waMonitor);
export const chatController = new ChatController(waMonitor); export const chatController = new ChatController(waMonitor);
export const businessController = new BusinessController(waMonitor);
export const groupController = new GroupController(waMonitor); export const groupController = new GroupController(waMonitor);
export const labelController = new LabelController(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 evolutionController = new EvolutionController(prismaRepository, waMonitor);
export const metaController = new MetaController(prismaRepository, waMonitor); export const metaController = new MetaController(prismaRepository, waMonitor);
export const baileysController = new BaileysController(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); 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); 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); 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); 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'); logger.info('Module - ON');

View File

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

View File

@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config'; import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions'; import { NotFoundException } from '@exceptions';
import { execSync } from 'child_process'; import { execFileSync } from 'child_process';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs'; import { rmSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
@ -91,6 +91,7 @@ export class WAMonitoringService {
Chatwoot: true, Chatwoot: true,
Proxy: true, Proxy: true,
Rabbitmq: true, Rabbitmq: true,
Nats: true,
Sqs: true, Sqs: true,
Websocket: true, Websocket: true,
Setting: true, Setting: true,
@ -168,7 +169,8 @@ export class WAMonitoringService {
public async cleaningStoreData(instanceName: string) { public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) { 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({ 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.chatwoot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.proxy.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.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.sqs.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } }); await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.typebot.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_UPDATE = 'messages.update',
MESSAGES_DELETE = 'messages.delete', MESSAGES_DELETE = 'messages.delete',
SEND_MESSAGE = 'send.message', SEND_MESSAGE = 'send.message',
SEND_MESSAGE_UPDATE = 'send.message.update',
CONTACTS_SET = 'contacts.set', CONTACTS_SET = 'contacts.set',
CONTACTS_UPSERT = 'contacts.upsert', CONTACTS_UPSERT = 'contacts.upsert',
CONTACTS_UPDATE = 'contacts.update', CONTACTS_UPDATE = 'contacts.update',

View File

@ -72,6 +72,7 @@ export type EventsRabbitmq = {
MESSAGES_UPDATE: boolean; MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean; MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean; SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean; CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean; CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean; CONTACTS_UPSERT: boolean;
@ -94,10 +95,20 @@ export type EventsRabbitmq = {
export type Rabbitmq = { export type Rabbitmq = {
ENABLED: boolean; ENABLED: boolean;
URI: string; URI: string;
FRAME_MAX: number;
EXCHANGE_NAME: string; EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean; GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq; 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 = { export type Sqs = {
@ -131,6 +142,7 @@ export type EventsWebhook = {
MESSAGES_UPDATE: boolean; MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean; MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean; SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean; CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean; CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean; CONTACTS_UPSERT: boolean;
@ -163,6 +175,7 @@ export type EventsPusher = {
MESSAGES_UPDATE: boolean; MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean; MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean; SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean; CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean; CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean; CONTACTS_UPSERT: boolean;
@ -220,7 +233,21 @@ export type CacheConfLocal = {
TTL: number; TTL: number;
}; };
export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; 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 Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string }; export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
export type QrCode = { LIMIT: number; COLOR: 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 Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
export type Dify = { ENABLED: boolean }; export type Dify = { ENABLED: boolean };
export type N8n = { ENABLED: boolean };
export type Evoai = { ENABLED: boolean };
export type Flowise = { ENABLED: boolean };
export type S3 = { export type S3 = {
ACCESS_KEY: string; ACCESS_KEY: string;
@ -251,6 +281,7 @@ export type S3 = {
PORT?: number; PORT?: number;
USE_SSL?: boolean; USE_SSL?: boolean;
REGION?: string; REGION?: string;
SKIP_POLICY?: boolean;
}; };
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
@ -263,6 +294,7 @@ export interface Env {
PROVIDER: ProviderSession; PROVIDER: ProviderSession;
DATABASE: Database; DATABASE: Database;
RABBITMQ: Rabbitmq; RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs; SQS: Sqs;
WEBSOCKET: Websocket; WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness; WA_BUSINESS: WaBusiness;
@ -278,6 +310,9 @@ export interface Env {
CHATWOOT: Chatwoot; CHATWOOT: Chatwoot;
OPENAI: Openai; OPENAI: Openai;
DIFY: Dify; DIFY: Dify;
N8N: N8n;
EVOAI: Evoai;
FLOWISE: Flowise;
CACHE: CacheConf; CACHE: CacheConf;
S3?: S3; S3?: S3;
AUTHENTICATION: Auth; AUTHENTICATION: Auth;
@ -356,9 +391,10 @@ export class ConfigService {
RABBITMQ: { RABBITMQ: {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true', ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_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', EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.RABBITMQ_URI || '', URI: process.env.RABBITMQ_URI || '',
FRAME_MAX: Number.parseInt(process.env.RABBITMQ_FRAME_MAX) || 8192,
EVENTS: { EVENTS: {
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true', APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === '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_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true', MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === '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_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true', CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === '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', 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: { SQS: {
ENABLED: process.env?.SQS_ENABLED === 'true', ENABLED: process.env?.SQS_ENABLED === 'true',
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '', 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_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true', MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === '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_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true', CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === '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_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === '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_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === '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: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '', 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: { CONFIG_SESSION_PHONE: {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
@ -533,6 +622,15 @@ export class ConfigService {
DIFY: { DIFY: {
ENABLED: process.env?.DIFY_ENABLED === 'true', 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: { CACHE: {
REDIS: { REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
@ -555,6 +653,7 @@ export class ConfigService {
PORT: Number.parseInt(process.env?.S3_PORT || '9000'), PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
USE_SSL: process.env?.S3_USE_SSL === 'true', USE_SSL: process.env?.S3_USE_SSL === 'true',
REGION: process.env?.S3_REGION, REGION: process.env?.S3_REGION,
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
}, },
AUTHENTICATION: { AUTHENTICATION: {
API_KEY: { API_KEY: {

View File

@ -128,7 +128,15 @@ async function bootstrap() {
const httpServer = configService.get<HttpServer>('SERVER'); const httpServer = configService.get<HttpServer>('SERVER');
ServerUP.app = app; 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); 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