mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-16 12:12:55 -06:00
Compare commits
No commits in common. "main" and "2.2.2" have entirely different histories.
@ -1,7 +1,5 @@
|
||||
.git
|
||||
*Dockerfile*
|
||||
*docker-compose*
|
||||
package-lock.json
|
||||
.env
|
||||
node_modules
|
||||
dist
|
26
.env.example
26
.env.example
@ -3,9 +3,6 @@ SERVER_PORT=8080
|
||||
# Server URL - Set your application url
|
||||
SERVER_URL=http://localhost:8080
|
||||
|
||||
SSL_CONF_PRIVKEY=/path/to/cert.key
|
||||
SSL_CONF_FULLCHAIN=/path/to/cert.crt
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
@ -50,7 +47,6 @@ DATABASE_DELETE_MESSAGE=true
|
||||
RABBITMQ_ENABLED=false
|
||||
RABBITMQ_URI=amqp://localhost
|
||||
RABBITMQ_EXCHANGE_NAME=evolution
|
||||
RABBITMQ_FRAME_MAX=8192
|
||||
# Global events - By enabling this variable, events from all instances are sent in the same event queue.
|
||||
RABBITMQ_GLOBAL_ENABLED=false
|
||||
# Prefix key to queue name
|
||||
@ -66,7 +62,6 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
|
||||
RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@ -113,7 +108,6 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
|
||||
PUSHER_EVENTS_MESSAGES_UPDATE=true
|
||||
PUSHER_EVENTS_MESSAGES_DELETE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
PUSHER_EVENTS_CONTACTS_SET=true
|
||||
PUSHER_EVENTS_CONTACTS_UPSERT=true
|
||||
PUSHER_EVENTS_CONTACTS_UPDATE=true
|
||||
@ -155,7 +149,6 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
@ -180,15 +173,6 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||
WEBHOOK_EVENTS_ERRORS=false
|
||||
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
|
||||
WEBHOOK_REQUEST_TIMEOUT_MS=60000
|
||||
WEBHOOK_RETRY_MAX_ATTEMPTS=10
|
||||
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
|
||||
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
|
||||
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
|
||||
WEBHOOK_RETRY_JITTER_FACTOR=0.2
|
||||
# Comma separated list of HTTP status codes that should not trigger retries
|
||||
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
|
||||
|
||||
# Name that will be displayed on smartphone connection
|
||||
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
||||
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
||||
@ -196,7 +180,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
# Whatsapp Web version for baileys channel
|
||||
# https://web.whatsapp.com/check-update?version=0&platform=web
|
||||
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
|
||||
CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
@ -226,12 +210,6 @@ OPENAI_ENABLED=false
|
||||
# Dify - Environment variables
|
||||
DIFY_ENABLED=false
|
||||
|
||||
# n8n - Environment variables
|
||||
N8N_ENABLED=false
|
||||
|
||||
# EvoAI - Environment variables
|
||||
EVOAI_ENABLED=false
|
||||
|
||||
# Cache - Environment variables
|
||||
# Redis Cache enabled
|
||||
CACHE_REDIS_ENABLED=true
|
||||
@ -288,4 +266,4 @@ LANGUAGE=en
|
||||
# PROXY_PORT=80
|
||||
# PROXY_PROTOCOL=http
|
||||
# PROXY_USERNAME=
|
||||
# PROXY_PASSWORD=
|
||||
# PROXY_PASSWORD=
|
2
.github/workflows/publish_docker_image.yml
vendored
2
.github/workflows/publish_docker_image.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: evoapicloud/evolution-api
|
||||
images: atendai/evolution-api
|
||||
tags: type=semver,pattern=v{{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
|
@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: evoapicloud/evolution-api
|
||||
images: atendai/evolution-api
|
||||
tags: homolog
|
||||
|
||||
- name: Set up QEMU
|
||||
|
@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: evoapicloud/evolution-api
|
||||
images: atendai/evolution-api
|
||||
tags: latest
|
||||
|
||||
- name: Set up QEMU
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,8 +2,6 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
.cursor*
|
||||
|
||||
/Docker/.env
|
||||
|
||||
.vscode
|
||||
|
38
CHANGELOG.md
38
CHANGELOG.md
@ -1,41 +1,3 @@
|
||||
# 2.3.0 (2025-06-17 09:19)
|
||||
|
||||
### Feature
|
||||
|
||||
* Add support to get Catalogs and Collections with new routes: '{{baseUrl}}/chat/fetchCatalogs' and '{{baseUrl}}/chat/fetchCollections'
|
||||
* Add NATS integration support to the event system
|
||||
* Add message location support meta
|
||||
* Add S3_SKIP_POLICY env variable to disable setBucketPolicy for incompatible providers
|
||||
* Add EvoAI integration with models, services, and routes
|
||||
* Add N8n integration with models, services, and routes
|
||||
|
||||
### Fixed
|
||||
|
||||
* Shell injection vulnerability
|
||||
* Update Baileys Version v6.7.18
|
||||
* Audio send duplicate from chatwoot
|
||||
* Chatwoot csat creating new conversation in another language
|
||||
* Refactor SQS controller to correct bug in sqs events by instance
|
||||
* Adjustin cloud api send audio and video
|
||||
* Preserve animation in GIF and WebP stickers
|
||||
* Preventing use conversation from other inbox for the same user
|
||||
* Ensure full WhatsApp compatibility for audio conversion (libopus, 48kHz, mono)
|
||||
* Enhance message fetching and processing logic
|
||||
* Added lid on whatsapp numbers router
|
||||
* Now if the CONFIG_SESSION_PHONE_VERSION variable is not filled in it automatically searches for the most updated version
|
||||
|
||||
### Security
|
||||
|
||||
* Change execSync to execFileSync
|
||||
* Enhance WebSocket authentication and connection handling
|
||||
|
||||
# 2.2.3 (2025-02-03 11:52)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix cache in local file system
|
||||
* Update Baileys Version
|
||||
|
||||
# 2.2.2 (2025-01-31 06:55)
|
||||
|
||||
### Features
|
||||
|
@ -2,7 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
evolution_v2:
|
||||
image: atendai/evolution-api:v2.2.3
|
||||
image: atendai/evolution-api:v2.1.2
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
@ -34,7 +34,6 @@ services:
|
||||
- RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
- RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@ -72,7 +71,6 @@ services:
|
||||
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
- WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
@ -94,7 +92,7 @@ services:
|
||||
- WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
|
||||
- CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
|
||||
- CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
|
||||
- QRCODE_LIMIT=30
|
||||
- OPENAI_ENABLED=true
|
||||
- DIFY_ENABLED=true
|
||||
|
@ -1,11 +1,11 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache git ffmpeg wget curl bash openssl
|
||||
apk add git ffmpeg wget curl bash openssl
|
||||
|
||||
LABEL version="2.3.0" description="Api to control whatsapp features through http requests."
|
||||
LABEL version="2.2.2" description="Api to control whatsapp features through http requests."
|
||||
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
|
||||
LABEL contact="contato@evolution-api.com"
|
||||
LABEL contact="contato@atendai.com"
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@ -8,7 +8,7 @@ a. LOGO and copyright information: In the process of using Evolution API's front
|
||||
|
||||
b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
|
||||
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
Please contact contato@atendai.com to inquire about licensing matters.
|
||||
|
||||
2. As a contributor, you should agree that:
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[]
|
||||
[](https://evolution-api.com/whatsapp)
|
||||
[](https://evolution-api.com/discord)
|
||||
[](https://evolution-api.com/postman)
|
||||
@ -88,7 +87,6 @@ https://github.com/sponsors/EvolutionAPI
|
||||
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
|
||||
|
||||
- [Promovaweb](https://www.youtube.com/@promovaweb)
|
||||
- [Sandeco](https://www.youtube.com/@canalsandeco)
|
||||
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
|
||||
- [Francis MNO](https://www.youtube.com/@FrancisMNO)
|
||||
- [Pablo Cabral](https://youtube.com/@pablocabral)
|
||||
@ -113,7 +111,7 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
|
||||
|
||||
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
|
||||
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
Please contact contato@atendai.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).
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: evoapicloud/evolution-api:latest
|
||||
image: atendai/evolution-api:homolog
|
||||
restart: always
|
||||
depends_on:
|
||||
- redis
|
||||
|
381
manager/dist/assets/index-CFAZX6IV.js
vendored
Normal file
381
manager/dist/assets/index-CFAZX6IV.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-CXH2BdD4.css
vendored
1
manager/dist/assets/index-CXH2BdD4.css
vendored
File diff suppressed because one or more lines are too long
381
manager/dist/assets/index-D-oOjDYe.js
vendored
381
manager/dist/assets/index-D-oOjDYe.js
vendored
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DNOCacL_.css
vendored
Normal file
1
manager/dist/assets/index-DNOCacL_.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
manager/dist/index.html
vendored
6
manager/dist/index.html
vendored
@ -2,11 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/assets/images/evolution-logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Evolution Manager</title>
|
||||
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
|
||||
<script type="module" crossorigin src="/assets/index-CFAZX6IV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
3271
package-lock.json
generated
3271
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.3.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
@ -41,7 +41,7 @@
|
||||
],
|
||||
"author": {
|
||||
"name": "Davidson Gomes",
|
||||
"email": "contato@evolution-api.com"
|
||||
"email": "contato@atendai.com"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@ -58,7 +58,6 @@
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@sentry/node": "^8.47.0",
|
||||
"amqplib": "^0.10.5",
|
||||
"audio-decode": "^2.2.3",
|
||||
"axios": "^1.7.9",
|
||||
"baileys": "github:EvolutionAPI/Baileys",
|
||||
"class-validator": "^0.14.1",
|
||||
@ -76,7 +75,6 @@
|
||||
"jimp": "^0.16.13",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"link-preview-js": "^3.0.13",
|
||||
"long": "^5.2.3",
|
||||
"mediainfo.js": "^0.3.4",
|
||||
@ -84,7 +82,6 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"minio": "^8.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nats": "^2.29.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.77.3",
|
||||
|
@ -1,9 +0,0 @@
|
||||
/*
|
||||
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);
|
@ -1,175 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `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`);
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
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);
|
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
@ -86,7 +86,6 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@ -100,13 +99,12 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
IntegrationSession IntegrationSession[]
|
||||
integrationSessions IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@ -118,19 +116,18 @@ model Session {
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels Json? @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels Json? @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@unique([instanceId, remoteJid])
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
@@unique([instanceId, remoteJid])
|
||||
}
|
||||
|
||||
model Contact {
|
||||
@ -173,7 +170,6 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -189,7 +185,6 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@ -206,7 +201,6 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -275,7 +269,6 @@ model Setting {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -289,16 +282,6 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
events Json @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
@ -399,7 +382,7 @@ model IntegrationSession {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @db.VarChar(500)
|
||||
fileName String @unique @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
@ -644,100 +627,3 @@ model IsOnWhatsapp {
|
||||
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
-- 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;
|
@ -1,62 +0,0 @@
|
||||
-- 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;
|
@ -1,61 +0,0 @@
|
||||
-- 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;
|
@ -1,2 +0,0 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Media_fileName_key";
|
@ -1,7 +0,0 @@
|
||||
-- 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;
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "IsOnWhatsapp" ADD COLUMN "lid" VARCHAR(100);
|
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
@ -86,7 +86,6 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@ -100,16 +99,12 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
IntegrationSession IntegrationSession[]
|
||||
integrationSessions IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
@ -130,7 +125,6 @@ model Chat {
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
@ -174,7 +168,6 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -190,7 +183,6 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@ -207,7 +199,6 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -278,7 +269,6 @@ model Setting {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -292,16 +282,6 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
@ -357,8 +337,6 @@ model Typebot {
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
TypebotSetting TypebotSetting[]
|
||||
@ -376,8 +354,6 @@ model TypebotSetting {
|
||||
debounceTime Int? @db.Integer
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
@ -387,7 +363,7 @@ model TypebotSetting {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @db.VarChar(500)
|
||||
fileName String @unique @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Date
|
||||
@ -648,104 +624,6 @@ model IsOnWhatsapp {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @unique @db.VarChar(100)
|
||||
jidOptions String
|
||||
lid String? @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -170,9 +170,6 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
@ -261,9 +258,6 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
|
@ -18,14 +18,6 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
|
||||
function isEmoji(str: string) {
|
||||
if (str === '') return true;
|
||||
|
||||
const emojiRegex =
|
||||
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
|
||||
return emojiRegex.test(str);
|
||||
}
|
||||
|
||||
export class SendMessageController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
@ -89,8 +81,8 @@ export class SendMessageController {
|
||||
}
|
||||
|
||||
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
|
||||
if (!isEmoji(data.reaction)) {
|
||||
throw new BadRequestException('Reaction must be a single emoji or empty string');
|
||||
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
|
||||
throw new BadRequestException('"reaction" must be an emoji');
|
||||
}
|
||||
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
export class NumberDto {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class getCatalogDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export class getCollectionsDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
}
|
@ -13,7 +13,6 @@ export class OnWhatsAppDto {
|
||||
public readonly exists: boolean,
|
||||
public readonly number: string,
|
||||
public readonly name?: string,
|
||||
public readonly lid?: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,6 @@ export class Metadata {
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
encoding?: boolean;
|
||||
notConvertSticker?: boolean;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
|
@ -165,7 +165,11 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
openAiDefaultSettings.speechToText &&
|
||||
received?.message?.audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
received,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import axios from 'axios';
|
||||
import { arrayUnique, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import FormData from 'form-data';
|
||||
import { createReadStream } from 'fs';
|
||||
import mimeTypes from 'mime-types';
|
||||
import { join } from 'path';
|
||||
|
||||
@ -146,20 +147,11 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${id}`;
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
|
||||
// Primeiro, obtenha a URL do arquivo
|
||||
let result = await axios.get(urlServer, { headers });
|
||||
|
||||
// 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',
|
||||
});
|
||||
|
||||
result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
this.logger.error(`Error downloading media: ${e}`);
|
||||
throw e;
|
||||
this.logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,23 +159,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const message = received.messages[0];
|
||||
let content: any = message.type + 'Message';
|
||||
content = { [content]: message[message.type] };
|
||||
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 } };
|
||||
}
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
return content;
|
||||
}
|
||||
|
||||
@ -216,77 +192,17 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
private messageTextJson(received: any) {
|
||||
// Verificar que received y received.messages existen
|
||||
if (!received || !received.messages || received.messages.length === 0) {
|
||||
this.logger.error('Error: received object or messages array is undefined or empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = received.messages[0];
|
||||
let content: any;
|
||||
|
||||
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
|
||||
if (!message.text) {
|
||||
// Si no hay texto, manejamos diferente según el tipo de mensaje
|
||||
if (message.type === 'sticker') {
|
||||
content = { stickerMessage: {} };
|
||||
} else if (message.type === 'location') {
|
||||
content = {
|
||||
locationMessage: {
|
||||
degreesLatitude: message.location?.latitude,
|
||||
degreesLongitude: message.location?.longitude,
|
||||
name: message.location?.name,
|
||||
address: message.location?.address,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
|
||||
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
|
||||
content = { [message.type + 'Message']: message[message.type] || {} };
|
||||
}
|
||||
|
||||
// Añadir contexto si existe
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Si el mensaje tiene texto, procesamos normalmente
|
||||
if (!received.metadata || !received.metadata.phone_number_id) {
|
||||
this.logger.error('Error: metadata or phone_number_id is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = received.messages[0];
|
||||
if (message.from === received.metadata.phone_number_id) {
|
||||
content = {
|
||||
extendedTextMessage: { text: message.text.body },
|
||||
};
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
} else {
|
||||
content = { conversation: message.text.body };
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -367,12 +283,6 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
case 'template':
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
case 'location':
|
||||
messageType = 'locationMessage';
|
||||
break;
|
||||
case 'sticker':
|
||||
messageType = 'stickerMessage';
|
||||
break;
|
||||
default:
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
@ -389,36 +299,17 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
if (received.contacts) pushName = received.contacts[0].profile.name;
|
||||
|
||||
if (received.messages) {
|
||||
const message = received.messages[0]; // Añadir esta línea para definir message
|
||||
|
||||
const key = {
|
||||
id: message.id,
|
||||
id: received.messages[0].id,
|
||||
remoteJid: this.phoneNumber,
|
||||
fromMe: message.from === received.metadata.phone_number_id,
|
||||
fromMe: received.messages[0].from === received.metadata.phone_number_id,
|
||||
};
|
||||
|
||||
if (message.type === 'sticker') {
|
||||
this.logger.log('Procesando mensaje de tipo sticker');
|
||||
if (this.isMediaMessage(received?.messages[0])) {
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
message: {
|
||||
stickerMessage: message.sticker || {},
|
||||
},
|
||||
messageType: 'stickerMessage',
|
||||
messageTimestamp: parseInt(message.timestamp) as number,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (this.isMediaMessage(message)) {
|
||||
const messageContent =
|
||||
message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
|
||||
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
message: messageContent,
|
||||
contextInfo: messageContent?.contextInfo,
|
||||
message: this.messageMediaJson(received),
|
||||
contextInfo: this.messageMediaJson(received)?.contextInfo,
|
||||
messageType: this.renderMessageType(received.messages[0].type),
|
||||
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
||||
source: 'unknown',
|
||||
@ -436,10 +327,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.get(urlServer, { headers });
|
||||
|
||||
const buffer = await axios.get(result.data.url, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
|
||||
|
||||
let mediaType;
|
||||
|
||||
@ -464,17 +352,6 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
// Para áudio, garantir extensão correta baseada no mimetype
|
||||
if (mediaType === 'audio') {
|
||||
if (mimetype.includes('ogg')) {
|
||||
fileName = `${message.messages[0].id}.ogg`;
|
||||
} else if (mimetype.includes('mp3')) {
|
||||
fileName = `${message.messages[0].id}.mp3`;
|
||||
} else if (mimetype.includes('m4a')) {
|
||||
fileName = `${message.messages[0].id}.m4a`;
|
||||
}
|
||||
}
|
||||
|
||||
const size = result.headers['content-length'] || buffer.data.byteLength;
|
||||
|
||||
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
||||
@ -501,72 +378,13 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
openAiDefaultSettings &&
|
||||
openAiDefaultSettings.openaiCredsId &&
|
||||
openAiDefaultSettings.speechToText
|
||||
) {
|
||||
try {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
)}`;
|
||||
} catch (speechError) {
|
||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
} else {
|
||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||
try {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
base64: messageRaw.message.base64,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
)}`;
|
||||
} catch (speechError) {
|
||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (received?.messages[0].interactive) {
|
||||
messageRaw = {
|
||||
@ -637,6 +455,37 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
// await this.client.readMessages([received.key]);
|
||||
}
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED) {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
const audioMessage = received?.messages[0]?.audio;
|
||||
|
||||
if (
|
||||
openAiDefaultSettings &&
|
||||
openAiDefaultSettings.openaiCredsId &&
|
||||
openAiDefaultSettings.speechToText &&
|
||||
audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
@ -662,7 +511,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
|
||||
if (!this.isMediaMessage(received?.messages[0])) {
|
||||
await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
@ -865,54 +714,17 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
protected async eventHandler(content: any) {
|
||||
try {
|
||||
// Registro para depuración
|
||||
this.logger.log('Contenido recibido en eventHandler:');
|
||||
this.logger.log(JSON.stringify(content, null, 2));
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
|
||||
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);
|
||||
}
|
||||
this.messageHandle(content, database, settings);
|
||||
}
|
||||
|
||||
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
||||
try {
|
||||
let quoted: any;
|
||||
let webhookUrl: any;
|
||||
const linkPreview = options?.linkPreview != false ? undefined : false;
|
||||
if (options?.quoted) {
|
||||
const m = options?.quoted;
|
||||
|
||||
@ -980,7 +792,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
text: {
|
||||
body: message['conversation'],
|
||||
preview_url: Boolean(options?.linkPreview),
|
||||
preview_url: linkPreview,
|
||||
},
|
||||
};
|
||||
quoted ? (content.context = { message_id: quoted.id }) : content;
|
||||
@ -996,10 +808,9 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
[message['mediaType']]: {
|
||||
[message['type']]: message['id'],
|
||||
...(message['mediaType'] !== 'audio' &&
|
||||
message['fileName'] &&
|
||||
!isImage && { filename: message['fileName'] }),
|
||||
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
||||
preview_url: linkPreview,
|
||||
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
|
||||
caption: message['caption'],
|
||||
},
|
||||
};
|
||||
quoted ? (content.context = { message_id: quoted.id }) : content;
|
||||
@ -1097,7 +908,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
})();
|
||||
|
||||
if (messageSent?.error_data || messageSent.message) {
|
||||
if (messageSent?.error_data) {
|
||||
this.logger.error(messageSent);
|
||||
return messageSent;
|
||||
}
|
||||
@ -1164,50 +975,29 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
return res;
|
||||
}
|
||||
|
||||
private async getIdMedia(mediaMessage: any, isFile = false) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
private async getIdMedia(mediaMessage: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
const fileStream = createReadStream(mediaMessage.media);
|
||||
|
||||
const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
|
||||
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
|
||||
formData.append('typeFile', mediaMessage.mimetype);
|
||||
formData.append('messaging_product', 'whatsapp');
|
||||
|
||||
formData.append('typeFile', mimetype);
|
||||
formData.append('messaging_product', 'whatsapp');
|
||||
// const fileBuffer = await fs.readFile(mediaMessage.media);
|
||||
|
||||
const token = this.token;
|
||||
// const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype });
|
||||
// formData.append('file', fileBlob);
|
||||
// formData.append('typeFile', mediaMessage.mimetype);
|
||||
// formData.append('messaging_product', 'whatsapp');
|
||||
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
|
||||
this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
|
||||
}/${this.number}/media`;
|
||||
|
||||
const res = await axios.post(url, formData, { headers });
|
||||
return res.data.id;
|
||||
} catch (error) {
|
||||
this.logger.error(error.response.data);
|
||||
throw new InternalServerErrorException(error?.toString() || error);
|
||||
}
|
||||
const headers = { Authorization: `Bearer ${this.token}` };
|
||||
const res = await axios.post(
|
||||
process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media',
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
return res.data.id;
|
||||
}
|
||||
|
||||
protected async prepareMediaMessage(mediaMessage: MediaMessage) {
|
||||
@ -1280,87 +1070,48 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
return mediaSent;
|
||||
}
|
||||
|
||||
public async processAudio(audio: string, number: string, file: any) {
|
||||
public async processAudio(audio: string, number: string) {
|
||||
number = number.replace(/\D/g, '');
|
||||
const hash = `${number}-${new Date().getTime()}`;
|
||||
|
||||
if (process.env.API_AUDIO_CONVERTER) {
|
||||
this.logger.verbose('Using audio converter API');
|
||||
const formData = new FormData();
|
||||
let mimetype: string | false;
|
||||
|
||||
if (file) {
|
||||
formData.append('file', file.buffer, {
|
||||
filename: file.originalname,
|
||||
contentType: file.mimetype,
|
||||
});
|
||||
} else if (isURL(audio)) {
|
||||
formData.append('url', audio);
|
||||
} else {
|
||||
formData.append('base64', audio);
|
||||
}
|
||||
|
||||
formData.append('format', 'mp3');
|
||||
|
||||
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
const audioConverter = response?.data?.audio || response?.data?.url;
|
||||
|
||||
if (!audioConverter) {
|
||||
throw new InternalServerErrorException('Failed to convert audio');
|
||||
}
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audioConverter,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mimeTypes.lookup(audio);
|
||||
prepareMedia.id = audio;
|
||||
prepareMedia.type = 'link';
|
||||
} else {
|
||||
mimetype = mimeTypes.lookup(prepareMedia.fileName);
|
||||
const id = await this.getIdMedia(prepareMedia);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
|
||||
this.logger.verbose('Audio converted');
|
||||
return prepareMedia;
|
||||
} else {
|
||||
let mimetype: string | false;
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mimeTypes.lookup(audio);
|
||||
prepareMedia.id = audio;
|
||||
prepareMedia.type = 'link';
|
||||
} else if (audio && !file) {
|
||||
mimetype = mimeTypes.lookup(prepareMedia.fileName);
|
||||
const id = await this.getIdMedia(prepareMedia);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
} else if (file) {
|
||||
prepareMedia.media = file;
|
||||
const id = await this.getIdMedia(prepareMedia, true);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
mimetype = file.mimetype;
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
|
||||
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
||||
const message = await this.processAudio(data.audio, data.number, file);
|
||||
const mediaData: SendAudioDto = { ...data };
|
||||
|
||||
if (file?.buffer) {
|
||||
mediaData.audio = file.buffer.toString('base64');
|
||||
} else if (isURL(mediaData.audio)) {
|
||||
// DO NOTHING
|
||||
// mediaData.audio = mediaData.audio;
|
||||
} else {
|
||||
console.error('El archivo no tiene buffer o file es undefined');
|
||||
throw new Error('File or buffer is undefined');
|
||||
}
|
||||
|
||||
const message = await this.processAudio(mediaData.audio, data.number);
|
||||
|
||||
const audioSent = await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,935 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
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>;
|
||||
}
|
@ -2,10 +2,8 @@ import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import {
|
||||
difyController,
|
||||
evoaiController,
|
||||
evolutionBotController,
|
||||
flowiseController,
|
||||
n8nController,
|
||||
openaiController,
|
||||
typebotController,
|
||||
} from '@api/server.module';
|
||||
@ -99,10 +97,6 @@ export class ChatbotController {
|
||||
|
||||
await difyController.emit(emitData);
|
||||
|
||||
await n8nController.emit(emitData);
|
||||
|
||||
await evoaiController.emit(emitData);
|
||||
|
||||
await flowiseController.emit(emitData);
|
||||
}
|
||||
|
||||
@ -179,7 +173,7 @@ export class ChatbotController {
|
||||
if (session) {
|
||||
if (session.status !== 'closed' && !session.botId) {
|
||||
this.logger.warn('Session is already opened in another integration');
|
||||
return null;
|
||||
return;
|
||||
} else if (!session.botId) {
|
||||
session = null;
|
||||
}
|
||||
@ -194,13 +188,13 @@ export class ChatbotController {
|
||||
instance: InstanceDto,
|
||||
session?: IntegrationSession,
|
||||
) {
|
||||
let findBot: any = null;
|
||||
let findBot: null;
|
||||
|
||||
if (!session) {
|
||||
findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
|
||||
|
||||
if (!findBot) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
findBot = await botRepository.findFirst({
|
||||
|
@ -4,10 +4,8 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou
|
||||
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
import { EvoaiRouter } from './evoai/routes/evoai.router';
|
||||
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
|
||||
import { FlowiseRouter } from './flowise/routes/flowise.router';
|
||||
import { N8nRouter } from './n8n/routes/n8n.router';
|
||||
|
||||
export class ChatbotRouter {
|
||||
public readonly router: Router;
|
||||
@ -21,7 +19,5 @@ export class ChatbotRouter {
|
||||
this.router.use('/openai', new OpenaiRouter(...guards).router);
|
||||
this.router.use('/dify', new DifyRouter(...guards).router);
|
||||
this.router.use('/flowise', new FlowiseRouter(...guards).router);
|
||||
this.router.use('/n8n', new N8nRouter(...guards).router);
|
||||
this.router.use('/evoai', new EvoaiRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
|
||||
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
|
||||
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
|
||||
export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema';
|
||||
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
|
||||
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
|
||||
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
|
||||
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';
|
||||
|
@ -295,57 +295,51 @@ export class ChatwootService {
|
||||
avatar_url?: string,
|
||||
jid?: string,
|
||||
) {
|
||||
try {
|
||||
const client = await this.clientCw(instance);
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn('client not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
let data: any = {};
|
||||
if (!isGroup) {
|
||||
data = {
|
||||
inbox_id: inboxId,
|
||||
name: name || phoneNumber,
|
||||
identifier: jid,
|
||||
avatar_url: avatar_url,
|
||||
};
|
||||
|
||||
if ((jid && jid.includes('@')) || !jid) {
|
||||
data['phone_number'] = `+${phoneNumber}`;
|
||||
}
|
||||
} 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);
|
||||
if (!client) {
|
||||
this.logger.warn('client not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
let data: any = {};
|
||||
if (!isGroup) {
|
||||
data = {
|
||||
inbox_id: inboxId,
|
||||
name: name || phoneNumber,
|
||||
identifier: jid,
|
||||
avatar_url: avatar_url,
|
||||
};
|
||||
|
||||
if ((jid && jid.includes('@')) || !jid) {
|
||||
data['phone_number'] = `+${phoneNumber}`;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
public async updateContact(instance: InstanceDto, id: number, data: any) {
|
||||
@ -549,240 +543,216 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
|
||||
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||
const maxWaitTime = 5000; // 5 secounds
|
||||
|
||||
try {
|
||||
// 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('--- Start createConversation ---');
|
||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||||
|
||||
// If it already exists in the cache, return conversationId
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
|
||||
this.logger.verbose(`Cache key: ${cacheKey}`);
|
||||
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
this.logger.verbose(`Cached conversation ID: ${conversationId}`);
|
||||
let conversationExists: conversation | boolean;
|
||||
try {
|
||||
conversationExists = await client.conversations.get({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversationId,
|
||||
});
|
||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting conversation: ${error}`);
|
||||
conversationExists = false;
|
||||
}
|
||||
if (!conversationExists) {
|
||||
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
||||
this.cache.delete(cacheKey);
|
||||
return await this.createConversation(instance, body);
|
||||
}
|
||||
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
// If lock already exists, wait until release or timeout
|
||||
if (await this.cache.has(lockKey)) {
|
||||
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
|
||||
const start = Date.now();
|
||||
while (await this.cache.has(lockKey)) {
|
||||
if (Date.now() - start > maxWaitTime) {
|
||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||
break;
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
return conversationId;
|
||||
const isGroup = body.key.remoteJid.includes('@g.us');
|
||||
this.logger.verbose(`Is group: ${isGroup}`);
|
||||
|
||||
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
|
||||
this.logger.verbose(`Chat ID: ${chatId}`);
|
||||
|
||||
let nameContact: string;
|
||||
|
||||
nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
this.logger.verbose(`Name contact: ${nameContact}`);
|
||||
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
|
||||
if (!filterInbox) {
|
||||
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
this.logger.verbose('Processing group conversation');
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||
await this.updateContact(instance, findParticipant.id, {
|
||||
name: body.pushName,
|
||||
avatar_url: picture_url.profilePictureUrl || null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Adquire lock
|
||||
await this.cache.set(lockKey, true, 30);
|
||||
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
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;
|
||||
}
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
|
||||
const client = await this.clientCw(instance);
|
||||
if (!client) return null;
|
||||
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);
|
||||
|
||||
const isGroup = remoteJid.includes('@g.us');
|
||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
if (!filterInbox) return null;
|
||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||
|
||||
if (isGroup) {
|
||||
this.logger.verbose(`Processing group conversation`);
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||
await this.updateContact(instance, findParticipant.id, {
|
||||
name: body.pushName,
|
||||
avatar_url: picture_url.profilePictureUrl || null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
filterInbox.id,
|
||||
isGroup,
|
||||
body.pushName,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
);
|
||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||
contact = await this.updateContact(instance, contact.id, {
|
||||
...(nameNeedsUpdate && { name: nameContact }),
|
||||
...(waProfilePictureFile === '' && { avatar: null }),
|
||||
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
chatId,
|
||||
filterInbox.id,
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
jid,
|
||||
);
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
this.logger.warn(`Contact not created or found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
||||
this.logger.verbose(`Contact ID: ${contactId}`);
|
||||
|
||||
const contactConversations = (await client.contacts.listConversations({
|
||||
accountId: this.provider.accountId,
|
||||
id: contactId,
|
||||
})) as any;
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error(`No conversations found or payload is undefined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let inboxConversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.inbox_id == filterInbox.id,
|
||||
} else {
|
||||
const jid = body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
chatId,
|
||||
filterInbox.id,
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
jid,
|
||||
);
|
||||
if (inboxConversation) {
|
||||
if (this.provider.reopenConversation) {
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
||||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
this.logger.warn('Contact not created or found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
||||
this.logger.verbose(`Contact ID: ${contactId}`);
|
||||
|
||||
const contactConversations = (await client.contacts.listConversations({
|
||||
accountId: this.provider.accountId,
|
||||
id: contactId,
|
||||
})) as any;
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error('No conversations found or payload is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contactConversations.payload.length) {
|
||||
let conversation: any;
|
||||
if (this.provider.reopenConversation) {
|
||||
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
|
||||
|
||||
if (this.provider.conversationPending && conversation.status !== 'open') {
|
||||
if (conversation) {
|
||||
await client.conversations.toggleStatus({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: inboxConversation.id,
|
||||
conversationId: conversation.id,
|
||||
data: {
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
inboxConversation = contactConversations.payload.find(
|
||||
(conversation) =>
|
||||
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
||||
}
|
||||
|
||||
if (inboxConversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||
this.cache.set(cacheKey, inboxConversation.id);
|
||||
return inboxConversation.id;
|
||||
}
|
||||
} else {
|
||||
conversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
|
||||
}
|
||||
|
||||
const data = {
|
||||
contact_id: contactId.toString(),
|
||||
inbox_id: filterInbox.id.toString(),
|
||||
};
|
||||
|
||||
if (this.provider.conversationPending) {
|
||||
data['status'] = 'pending';
|
||||
if (conversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
}
|
||||
|
||||
/*
|
||||
Triple check after lock
|
||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||||
*/
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const conversation = await client.conversations.create({
|
||||
accountId: this.provider.accountId,
|
||||
data,
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.warn(`Conversation not created or found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
} finally {
|
||||
await this.cache.delete(lockKey);
|
||||
this.logger.verbose(`Block released for: ${lockKey}`);
|
||||
}
|
||||
|
||||
const data = {
|
||||
contact_id: contactId.toString(),
|
||||
inbox_id: filterInbox.id.toString(),
|
||||
};
|
||||
|
||||
if (this.provider.conversationPending) {
|
||||
data['status'] = 'pending';
|
||||
}
|
||||
|
||||
const conversation = await client.conversations.create({
|
||||
accountId: this.provider.accountId,
|
||||
data,
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.warn('Conversation not created or found');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in createConversation: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -961,7 +931,7 @@ export class ChatwootService {
|
||||
quotedMsg?: MessageModel,
|
||||
) {
|
||||
if (sourceId && this.isImportHistoryAvailable()) {
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
|
||||
if (messageAlreadySaved) {
|
||||
if (messageAlreadySaved.size > 0) {
|
||||
this.logger.warn('Message already saved on chatwoot');
|
||||
@ -1136,13 +1106,12 @@ export class ChatwootService {
|
||||
|
||||
sendTelemetry('/message/sendWhatsAppAudio');
|
||||
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, true);
|
||||
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||
if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
|
||||
type = 'document';
|
||||
}
|
||||
|
||||
@ -1684,7 +1653,7 @@ export class ChatwootService {
|
||||
stickerMessage: undefined,
|
||||
documentMessage: msg.documentMessage?.caption,
|
||||
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
|
||||
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
|
||||
audioMessage: msg.audioMessage?.caption,
|
||||
contactMessage: msg.contactMessage?.vcard,
|
||||
contactsArrayMessage: msg.contactsArrayMessage,
|
||||
locationMessage: msg.locationMessage,
|
||||
@ -1929,7 +1898,7 @@ export class ChatwootService {
|
||||
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
|
||||
: originalMessage;
|
||||
|
||||
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
|
||||
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2230,7 +2199,7 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||
if (event === 'messages.edit') {
|
||||
const editedText = `${
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||||
|
@ -169,7 +169,7 @@ class ChatwootImport {
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise<Set<string>> {
|
||||
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
|
||||
try {
|
||||
const existingSourceIdsSet = new Set<string>();
|
||||
|
||||
@ -177,25 +177,18 @@ class ChatwootImport {
|
||||
return existingSourceIdsSet;
|
||||
}
|
||||
|
||||
// Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
|
||||
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
const result = await pgClient.query(query, [formattedSourceIds]);
|
||||
|
||||
const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds];
|
||||
|
||||
const query = conversationId
|
||||
? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'
|
||||
: 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
|
||||
const result = await pgClient.query(query, params);
|
||||
for (const row of result.rows) {
|
||||
existingSourceIdsSet.add(row.source_id);
|
||||
}
|
||||
|
||||
return existingSourceIdsSet;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`);
|
||||
return new Set<string>();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -506,30 +499,25 @@ class ChatwootImport {
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
|
||||
switch (typeKey) {
|
||||
case 'documentMessage': {
|
||||
const doc = msg.message.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
|
||||
case 'documentWithCaptionMessage': {
|
||||
const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
|
||||
case 'templateMessage': {
|
||||
const template = msg.message.templateMessage?.hydratedTemplate;
|
||||
return (
|
||||
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
|
||||
(template?.hydratedContentText || '')
|
||||
);
|
||||
}
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
|
||||
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
@ -6,11 +7,12 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Dify } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
|
||||
import { Dify as DifyModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
|
||||
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
export class DifyController extends ChatbotController implements ChatbotControllerInterface {
|
||||
constructor(
|
||||
private readonly difyService: DifyService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -24,7 +26,6 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('DifyController');
|
||||
protected readonly integrationName = 'Dify';
|
||||
|
||||
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
|
||||
botRepository: any;
|
||||
@ -32,37 +33,261 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating dify');
|
||||
}
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'difyIdFallback';
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'dify';
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
@ -78,10 +303,81 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating dify');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add Dify-specific validation
|
||||
public async createBot(instance: InstanceDto, data: DifyDto) {
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
@ -92,35 +388,501 @@ export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Dify-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting dify bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Dify-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: DifyModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
difyIdFallback: updateSettings.difyIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
difyIdFallback: newSetttings.difyIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
difyIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
difyIdFallback: settings.difyIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.difyIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.difyIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,38 @@
|
||||
import { $Enums } from '@prisma/client';
|
||||
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class DifyDto extends BaseChatbotDto {
|
||||
export class DifyDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
botType?: $Enums.DifyBotType;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class DifySettingDto extends BaseChatbotSettingDto {
|
||||
export class DifySettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
difyIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,34 +1,60 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
export class DifyService {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'DifyService', configService);
|
||||
this.openaiService = openaiService;
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('DifyService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for Dify
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'dify';
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
protected async sendMessageToBot(
|
||||
private isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
@ -36,30 +62,10 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
) {
|
||||
try {
|
||||
let endpoint: string = dify.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Dify endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
let processedContent = content;
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dify.botType === 'chatBot') {
|
||||
endpoint += '/chat-messages';
|
||||
const payload: any = {
|
||||
@ -68,17 +74,17 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
query: processedContent,
|
||||
query: content,
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -106,9 +112,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -126,21 +130,21 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
endpoint += '/completion-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: processedContent,
|
||||
query: content,
|
||||
pushName: pushName,
|
||||
remoteJid: remoteJid,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -168,9 +172,7 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -192,17 +194,17 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
query: processedContent,
|
||||
query: content,
|
||||
response_mode: 'streaming',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -222,32 +224,113 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
let conversationId;
|
||||
let answer = '';
|
||||
|
||||
const data = response.data.replaceAll('data: ', '');
|
||||
const events = data.split('\n').filter((line) => line.trim() !== '');
|
||||
const stream = response.data;
|
||||
const reader = new Readable().wrap(stream);
|
||||
|
||||
for (const eventString of events) {
|
||||
if (eventString.trim().startsWith('{')) {
|
||||
const event = JSON.parse(eventString);
|
||||
reader.on('data', (chunk) => {
|
||||
const data = chunk.toString().replace(/data:\s*/g, '');
|
||||
|
||||
if (event?.event === 'agent_message') {
|
||||
console.log('event:', event);
|
||||
conversationId = conversationId ?? event?.conversation_id;
|
||||
answer += event?.answer;
|
||||
}
|
||||
if (data.trim() === '' || !data.startsWith('{')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const events = data.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
for (const eventString of events) {
|
||||
if (eventString.trim().startsWith('{')) {
|
||||
const event = JSON.parse(eventString);
|
||||
|
||||
if (event?.event === 'agent_message') {
|
||||
console.log('event:', event);
|
||||
conversationId = conversationId ?? event?.conversation_id;
|
||||
answer += event?.answer;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing stream data:', error);
|
||||
}
|
||||
});
|
||||
|
||||
reader.on('end', async () => {
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = answer;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: conversationId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
reader.on('error', (error) => {
|
||||
console.error('Error reading stream:', error);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if (answer) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
|
||||
}
|
||||
const message = response?.data?.data.outputs.text;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -256,13 +339,309 @@ export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.response?.data || error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
settings: DifySetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: dify.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processDify(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvoaiDto extends BaseChatbotDto {
|
||||
agentUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class EvoaiSettingDto extends BaseChatbotSettingDto {
|
||||
evoaiIdFallback?: string;
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
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();
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
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'),
|
||||
};
|
@ -1,13 +1,16 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { EvolutionBot, IntegrationSession } from '@prisma/client';
|
||||
import { EvolutionBot } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { EvolutionBotDto } from '../dto/evolutionBot.dto';
|
||||
import { EvolutionBotService } from '../services/evolutionBot.service';
|
||||
|
||||
export class EvolutionBotController extends BaseChatbotController<EvolutionBot, EvolutionBotDto> {
|
||||
export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
constructor(
|
||||
private readonly evolutionBotService: EvolutionBotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -21,49 +24,258 @@ export class EvolutionBotController extends BaseChatbotController<EvolutionBot,
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvolutionBotController');
|
||||
protected readonly integrationName = 'EvolutionBot';
|
||||
|
||||
integrationEnabled = true; // Set to true by default or use config value if available
|
||||
integrationEnabled: boolean;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Implementation of abstract methods required by BaseChatbotController
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.botIdFallback;
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'botIdFallback';
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'evolution';
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(
|
||||
botId: string,
|
||||
instanceId: string,
|
||||
data: EvolutionBotDto,
|
||||
): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
@ -76,21 +288,573 @@ export class EvolutionBotController extends BaseChatbotController<EvolutionBot,
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evolution Bot already exists');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Process bot-specific logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
botIdFallback: updateSettings.botIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
botIdFallback: newSetttings.botIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
botIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
botIdFallback: settings.botIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvolutionBot;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.botIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.botIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,37 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class EvolutionBotDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
export class EvolutionBotDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class EvolutionBotSettingDto extends BaseChatbotSettingDto {
|
||||
export class EvolutionBotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
botIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,138 +1,428 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvolutionBotService extends BaseChatbotService<EvolutionBot, EvolutionBotSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
export class EvolutionBotService {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
|
||||
this.openaiService = openaiService;
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('EvolutionBotService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bot type identifier
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evolution';
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the Evolution Bot API
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
private async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
bot: EvolutionBot,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
sessionId: session.id,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
) {
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
sessionId: session.id,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
query: content,
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
},
|
||||
query: content,
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
];
|
||||
payload.query = contentSplit[2] || content;
|
||||
}
|
||||
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.query = `[audio] ${transcription}`;
|
||||
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, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.message;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (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,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
|
||||
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})`;
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
payload.files = [
|
||||
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(
|
||||
{
|
||||
type: 'image',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
];
|
||||
payload.query = contentSplit[2] || content;
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
sendTelemetry('/message/sendText');
|
||||
|
||||
const endpoint = bot.apiUrl;
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Evolution Bot endpoint defined');
|
||||
return;
|
||||
}
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
settings: EvolutionBotSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
let headers: any = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
if (bot.apiKey) {
|
||||
headers = {
|
||||
...headers,
|
||||
Authorization: `Bearer ${bot.apiKey}`,
|
||||
};
|
||||
}
|
||||
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
if (!message) return;
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
let message = response?.data?.message;
|
||||
return;
|
||||
}
|
||||
|
||||
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)}`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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, session, bot, remoteJid, pushName, content);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Flowise } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
import { Flowise } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { FlowiseDto } from '../dto/flowise.dto';
|
||||
import { FlowiseService } from '../services/flowise.service';
|
||||
|
||||
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
|
||||
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
|
||||
constructor(
|
||||
private readonly flowiseService: FlowiseService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -24,73 +24,15 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('FlowiseController');
|
||||
protected readonly integrationName = 'Flowise';
|
||||
|
||||
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
|
||||
integrationEnabled: boolean;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
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
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: FlowiseDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
@ -99,7 +41,74 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Flowise-specific duplicate check
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a Flowise with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
@ -112,7 +121,740 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: FlowiseDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Bot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
flowiseIdFallback: updateSettings.flowiseIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
flowiseIdFallback: newSetttings.flowiseIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
flowiseIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
flowiseIdFallback: settings.flowiseIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as Flowise;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.flowiseIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.flowiseIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,37 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class FlowiseDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
export class FlowiseDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
||||
export class FlowiseSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
flowiseIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,57 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
export class FlowiseService {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
||||
this.openaiService = openaiService;
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('FlowiseService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the bot type for Flowise
|
||||
protected getBotType(): string {
|
||||
return 'flowise';
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
// 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> {
|
||||
private async sendMessageToBot(instance: any, bot: Flowise, remoteJid: string, pushName: string, content: string) {
|
||||
const payload: any = {
|
||||
question: content,
|
||||
overrideConfig: {
|
||||
@ -61,24 +54,11 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.question = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
@ -111,26 +91,335 @@ export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
|
||||
const endpoint = bot.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Flowise endpoint defined');
|
||||
return;
|
||||
}
|
||||
if (!endpoint) return null;
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
const message = response?.data?.text;
|
||||
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
// The service is now complete with just the abstract method implementations
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
settings: FlowiseSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,6 @@ export const flowiseSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||
@ -71,9 +69,7 @@ export const flowiseSettingSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
flowiseIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
botIdFallback: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
|
@ -1,127 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
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();
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
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'),
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,15 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class OpenaiDto extends BaseChatbotDto {
|
||||
export class OpenaiDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
openaiCredsId: string;
|
||||
botType: string;
|
||||
botType?: string;
|
||||
assistantId?: string;
|
||||
functionUrl?: string;
|
||||
model?: string;
|
||||
@ -15,10 +17,35 @@ export class OpenaiDto extends BaseChatbotDto {
|
||||
assistantMessages?: string[];
|
||||
userMessages?: string[];
|
||||
maxTokens?: number;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class OpenaiSettingDto extends BaseChatbotSettingDto {
|
||||
export class OpenaiSettingDto {
|
||||
openaiCredsId?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
openaiIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
speechToText?: boolean;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
|
||||
execute: (instance) => openaiController.getModels(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
@ -7,12 +8,13 @@ import { Events } from '@api/types/wa.types';
|
||||
import { configService, Typebot } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
|
||||
import { Typebot as TypebotModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller';
|
||||
|
||||
export class TypebotController extends BaseChatbotController<TypebotModel, TypebotDto> {
|
||||
export class TypebotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
constructor(
|
||||
private readonly typebotService: TypebotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -26,7 +28,6 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('TypebotController');
|
||||
protected readonly integrationName = 'Typebot';
|
||||
|
||||
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
|
||||
botRepository: any;
|
||||
@ -34,35 +35,245 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.typebotIdFallback;
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating typebot');
|
||||
}
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'typebotIdFallback';
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'typebot';
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error(
|
||||
'You already have a typebot with an "All" trigger, you cannot have more bots while it is active',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
@ -77,41 +288,263 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating typebot');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Typebot-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: TypebotModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Map to the original processTypebot method signature
|
||||
await this.typebotService.processTypebot(
|
||||
instance,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
bot,
|
||||
bot.url,
|
||||
settings.expire,
|
||||
bot.typebot,
|
||||
settings.keywordFinish,
|
||||
settings.delayMessage,
|
||||
settings.unknownMessage,
|
||||
settings.listeningFromMe,
|
||||
settings.stopBotFromMe,
|
||||
settings.keepOpen,
|
||||
content,
|
||||
{}, // prefilledVariables (optional)
|
||||
);
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { typebot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting typebot');
|
||||
}
|
||||
}
|
||||
|
||||
// TypeBot specific method for starting a bot from API
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
typebotIdFallback: updateSettings.typebotIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
typebotIdFallback: newSetttings.typebotIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
typebotIdFallback: null,
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
typebotIdFallback: settings.typebotIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async startBot(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
@ -119,7 +552,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
id: instance.instanceId,
|
||||
name: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
@ -228,12 +661,11 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
},
|
||||
});
|
||||
|
||||
// Use the original processTypebot method with all parameters
|
||||
await this.typebotService.processTypebot(
|
||||
this.waMonitor.waInstances[instanceData.name],
|
||||
instanceData,
|
||||
remoteJid,
|
||||
null, // msg
|
||||
null, // session
|
||||
null,
|
||||
null,
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
@ -290,7 +722,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
@ -315,4 +747,325 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class PrefilledVariables {
|
||||
remoteJid?: string;
|
||||
@ -7,11 +7,34 @@ export class PrefilledVariables {
|
||||
additionalData?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class TypebotDto extends BaseChatbotDto {
|
||||
export class TypebotDto {
|
||||
enabled?: boolean;
|
||||
description?: 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 extends BaseChatbotSettingDto {
|
||||
export class TypebotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
typebotIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -132,7 +132,6 @@ export class EventController {
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
@ -152,8 +151,5 @@ export class EventController {
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'REMOVE_INSTANCE',
|
||||
'LOGOUT_INSTANCE',
|
||||
'INSTANCE_CREATE',
|
||||
'INSTANCE_DELETE',
|
||||
'STATUS_INSTANCE',
|
||||
];
|
||||
}
|
||||
|
@ -26,11 +26,6 @@ export class EventDto {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
nats?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
pusher?: {
|
||||
enabled?: boolean;
|
||||
appId?: string;
|
||||
@ -68,11 +63,6 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
nats?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
pusher?: {
|
||||
enabled?: boolean;
|
||||
appId?: string;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { NatsController } from '@api/integrations/event/nats/nats.controller';
|
||||
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
|
||||
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
|
||||
import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
|
||||
@ -14,7 +13,6 @@ export class EventManager {
|
||||
private websocketController: WebsocketController;
|
||||
private webhookController: WebhookController;
|
||||
private rabbitmqController: RabbitmqController;
|
||||
private natsController: NatsController;
|
||||
private sqsController: SqsController;
|
||||
private pusherController: PusherController;
|
||||
|
||||
@ -25,7 +23,6 @@ export class EventManager {
|
||||
this.websocket = new WebsocketController(prismaRepository, waMonitor);
|
||||
this.webhook = new WebhookController(prismaRepository, waMonitor);
|
||||
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
|
||||
this.nats = new NatsController(prismaRepository, waMonitor);
|
||||
this.sqs = new SqsController(prismaRepository, waMonitor);
|
||||
this.pusher = new PusherController(prismaRepository, waMonitor);
|
||||
}
|
||||
@ -70,14 +67,6 @@ export class EventManager {
|
||||
return this.rabbitmqController;
|
||||
}
|
||||
|
||||
public set nats(nats: NatsController) {
|
||||
this.natsController = nats;
|
||||
}
|
||||
|
||||
public get nats() {
|
||||
return this.natsController;
|
||||
}
|
||||
|
||||
public set sqs(sqs: SqsController) {
|
||||
this.sqsController = sqs;
|
||||
}
|
||||
@ -96,7 +85,6 @@ export class EventManager {
|
||||
public init(httpServer: Server): void {
|
||||
this.websocket.init(httpServer);
|
||||
this.rabbitmq.init();
|
||||
this.nats.init();
|
||||
this.sqs.init();
|
||||
this.pusher.init();
|
||||
}
|
||||
@ -115,7 +103,6 @@ export class EventManager {
|
||||
}): Promise<void> {
|
||||
await this.websocket.emit(eventData);
|
||||
await this.rabbitmq.emit(eventData);
|
||||
await this.nats.emit(eventData);
|
||||
await this.sqs.emit(eventData);
|
||||
await this.webhook.emit(eventData);
|
||||
await this.pusher.emit(eventData);
|
||||
@ -138,14 +125,6 @@ export class EventManager {
|
||||
},
|
||||
});
|
||||
|
||||
if (data.nats)
|
||||
await this.nats.set(instanceName, {
|
||||
nats: {
|
||||
enabled: true,
|
||||
events: data.nats?.events,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.sqs)
|
||||
await this.sqs.set(instanceName, {
|
||||
sqs: {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
|
||||
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
|
||||
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
|
||||
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
|
||||
@ -15,7 +14,6 @@ export class EventRouter {
|
||||
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
|
||||
this.router.use('/websocket', new WebsocketRouter(...guards).router);
|
||||
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
|
||||
this.router.use('/nats', new NatsRouter(...guards).router);
|
||||
this.router.use('/pusher', new PusherRouter(...guards).router);
|
||||
this.router.use('/sqs', new SqsRouter(...guards).router);
|
||||
}
|
||||
|
@ -16,9 +16,6 @@ export const eventSchema: JSONSchema7 = {
|
||||
rabbitmq: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
nats: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
sqs: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
|
@ -1,161 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { EventDto } from '@api/integrations/event/event.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { eventManager } from '@api/server.module';
|
||||
import { eventSchema, instanceSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class 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();
|
||||
}
|
@ -21,21 +21,9 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
|
||||
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
|
||||
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
|
||||
|
||||
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) => {
|
||||
amqp.connect(uri, (error, connection) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
|
||||
import { SQS } from '@aws-sdk/client-sqs';
|
||||
import { configService, Log, Sqs } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||
import { EventDto } from '../event.dto';
|
||||
|
||||
export class SqsController extends EventController implements EventControllerInterface {
|
||||
private sqs: SQS;
|
||||
@ -46,39 +45,6 @@ export class SqsController extends EventController implements EventControllerInt
|
||||
return this.sqs;
|
||||
}
|
||||
|
||||
override async set(instanceName: string, data: EventDto): Promise<any> {
|
||||
if (!this.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data[this.name]?.enabled) {
|
||||
data[this.name].events = [];
|
||||
} else {
|
||||
if (0 === data[this.name].events.length) {
|
||||
data[this.name].events = EventController.events;
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled);
|
||||
|
||||
const payload: any = {
|
||||
where: {
|
||||
instanceId: this.monitor.waInstances[instanceName].instanceId,
|
||||
},
|
||||
update: {
|
||||
enabled: data[this.name]?.enabled,
|
||||
events: data[this.name].events,
|
||||
},
|
||||
create: {
|
||||
enabled: data[this.name]?.enabled,
|
||||
events: data[this.name].events,
|
||||
instanceId: this.monitor.waInstances[instanceName].instanceId,
|
||||
},
|
||||
};
|
||||
console.log('*** payload: ', payload);
|
||||
return this.prisma[this.name].upsert(payload);
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instanceName,
|
||||
origin,
|
||||
@ -155,92 +121,70 @@ export class SqsController extends EventController implements EventControllerInt
|
||||
}
|
||||
}
|
||||
|
||||
private async saveQueues(instanceName: string, events: string[], enable: boolean) {
|
||||
if (enable) {
|
||||
const eventsFinded = await this.listQueuesByInstance(instanceName);
|
||||
console.log('eventsFinded', eventsFinded);
|
||||
public async initQueues(instanceName: string, events: string[]) {
|
||||
if (!events || !events.length) return;
|
||||
|
||||
for (const event of events) {
|
||||
const normalizedEvent = event.toLowerCase();
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
|
||||
if (eventsFinded.includes(normalizedEvent)) {
|
||||
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
|
||||
continue;
|
||||
}
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
|
||||
const queueName = `${instanceName}_${normalizedEvent}.fifo`;
|
||||
|
||||
try {
|
||||
const createCommand = new CreateQueueCommand({
|
||||
QueueName: queueName,
|
||||
Attributes: {
|
||||
FifoQueue: 'true',
|
||||
},
|
||||
});
|
||||
const data = await this.sqs.send(createCommand);
|
||||
this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sqs.createQueue(
|
||||
{
|
||||
QueueName: queueName,
|
||||
Attributes: {
|
||||
FifoQueue: 'true',
|
||||
},
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error creating queue ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async listQueuesByInstance(instanceName: string) {
|
||||
let existingQueues: string[] = [];
|
||||
try {
|
||||
const listCommand = new ListQueuesCommand({
|
||||
QueueNamePrefix: `${instanceName}_`,
|
||||
});
|
||||
const listData = await this.sqs.send(listCommand);
|
||||
if (listData.QueueUrls && listData.QueueUrls.length > 0) {
|
||||
// Extrai o nome da fila a partir da URL
|
||||
existingQueues = listData.QueueUrls.map((queueUrl) => {
|
||||
const parts = queueUrl.split('/');
|
||||
return parts[parts.length - 1];
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
public async removeQueues(instanceName: string, events: any) {
|
||||
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
|
||||
if (!events || !eventsArray.length) return;
|
||||
|
||||
// Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
|
||||
return existingQueues
|
||||
.map((queueName) => {
|
||||
// Espera-se que o nome seja `${instanceName}_${event}.fifo`
|
||||
if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) {
|
||||
return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase();
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter((event) => event !== '');
|
||||
}
|
||||
const queues = eventsArray.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
|
||||
// Para uma futura feature de exclusão forçada das queues
|
||||
private async removeQueuesByInstance(instanceName: string) {
|
||||
try {
|
||||
const listCommand = new ListQueuesCommand({
|
||||
QueueNamePrefix: `${instanceName}_`,
|
||||
});
|
||||
const listData = await this.sqs.send(listCommand);
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
|
||||
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
|
||||
this.logger.info(`No queues found for instance ${instanceName}`);
|
||||
return;
|
||||
}
|
||||
this.sqs.getQueueUrl(
|
||||
{
|
||||
QueueName: queueName,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
const queueUrl = data.QueueUrl;
|
||||
|
||||
for (const queueUrl of listData.QueueUrls) {
|
||||
try {
|
||||
const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
|
||||
await this.sqs.send(deleteCommand);
|
||||
this.logger.info(`Queue ${queueUrl} deleted`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`);
|
||||
}
|
||||
this.sqs.deleteQueue(
|
||||
{
|
||||
QueueUrl: queueUrl,
|
||||
},
|
||||
(deleteErr) => {
|
||||
if (deleteErr) {
|
||||
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
|
||||
} else {
|
||||
this.logger.info(`Queue ${queueName} deleted`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { wa } from '@api/types/wa.types';
|
||||
import { configService, Log, Webhook } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
// import { BadRequestException } from '@exceptions';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||
|
||||
@ -18,9 +18,9 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
|
||||
// if (!/^(https?:\/\/)/.test(data.webhook.url)) {
|
||||
// throw new BadRequestException('Invalid "url" property');
|
||||
// }
|
||||
if (!isURL(data.webhook.url, { require_tld: false })) {
|
||||
throw new BadRequestException('Invalid "url" property');
|
||||
}
|
||||
|
||||
if (!data.webhook?.enabled) {
|
||||
data.webhook.events = [];
|
||||
@ -74,20 +74,10 @@ export class WebhookController extends EventController implements EventControlle
|
||||
|
||||
const webhookConfig = configService.get<Webhook>('WEBHOOK');
|
||||
const webhookLocal = instance?.events;
|
||||
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 webhookHeaders = instance?.headers;
|
||||
const we = event.replace(/[.-]/gm, '_').toUpperCase();
|
||||
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
|
||||
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||
const regex = /^(https?:\/\/)/;
|
||||
|
||||
const webhookData = {
|
||||
event,
|
||||
@ -121,11 +111,10 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance?.enabled && regex.test(instance.url)) {
|
||||
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
|
||||
const httpService = axios.create({
|
||||
baseURL,
|
||||
headers: webhookHeaders as Record<string, string> | undefined,
|
||||
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||
});
|
||||
|
||||
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
|
||||
@ -166,11 +155,8 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
try {
|
||||
if (regex.test(globalURL)) {
|
||||
const httpService = axios.create({
|
||||
baseURL: globalURL,
|
||||
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||
});
|
||||
if (isURL(globalURL)) {
|
||||
const httpService = axios.create({ baseURL: globalURL });
|
||||
|
||||
await this.retryWebhookRequest(
|
||||
httpService,
|
||||
@ -204,20 +190,12 @@ export class WebhookController extends EventController implements EventControlle
|
||||
origin: string,
|
||||
baseURL: string,
|
||||
serverUrl: string,
|
||||
maxRetries?: number,
|
||||
delaySeconds?: number,
|
||||
maxRetries = 10,
|
||||
delaySeconds = 30,
|
||||
): Promise<void> {
|
||||
const webhookConfig = configService.get<Webhook>('WEBHOOK');
|
||||
const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10;
|
||||
const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5;
|
||||
const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true;
|
||||
const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300;
|
||||
const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2;
|
||||
const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422];
|
||||
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxRetryAttempts) {
|
||||
while (attempts < maxRetries) {
|
||||
try {
|
||||
await httpService.post('', webhookData);
|
||||
if (attempts > 0) {
|
||||
@ -231,27 +209,12 @@ export class WebhookController extends EventController implements EventControlle
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
|
||||
const isTimeout = error.code === 'ECONNABORTED';
|
||||
|
||||
if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) {
|
||||
this.logger.error({
|
||||
local: `${origin}`,
|
||||
message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`,
|
||||
statusCode: error?.response?.status,
|
||||
url: baseURL,
|
||||
server_url: serverUrl,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
local: `${origin}`,
|
||||
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
|
||||
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
|
||||
hostName: error?.hostname,
|
||||
syscall: error?.syscall,
|
||||
code: error?.code,
|
||||
isTimeout,
|
||||
statusCode: error?.response?.status,
|
||||
error: error?.errno,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
@ -259,46 +222,12 @@ export class WebhookController extends EventController implements EventControlle
|
||||
server_url: serverUrl,
|
||||
});
|
||||
|
||||
if (attempts === maxRetryAttempts) {
|
||||
if (attempts === maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
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));
|
||||
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Auth, configService, Cors, Log, Websocket } from '@config/env.config';
|
||||
import { configService, Cors, Log, Websocket } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Server } from 'http';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
@ -24,40 +24,8 @@ export class WebsocketController extends EventController implements EventControl
|
||||
}
|
||||
|
||||
this.socket = new SocketIO(httpServer, {
|
||||
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);
|
||||
}
|
||||
cors: {
|
||||
origin: this.cors,
|
||||
},
|
||||
});
|
||||
|
||||
@ -133,7 +101,10 @@ export class WebsocketController extends EventController implements EventControl
|
||||
this.socket.emit(event, message);
|
||||
|
||||
if (logEnabled) {
|
||||
this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
|
||||
this.logger.log({
|
||||
local: `${origin}.sendData-WebsocketGlobal`,
|
||||
...message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +119,10 @@ export class WebsocketController extends EventController implements EventControl
|
||||
this.socket.of(`/${instanceName}`).emit(event, message);
|
||||
|
||||
if (logEnabled) {
|
||||
this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
|
||||
this.logger.log({
|
||||
local: `${origin}.sendData-Websocket`,
|
||||
...message,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -63,9 +63,9 @@ const createBucket = async () => {
|
||||
if (!exists) {
|
||||
await minioClient.makeBucket(bucketName);
|
||||
}
|
||||
if (!BUCKET.SKIP_POLICY) {
|
||||
await setBucketPolicy();
|
||||
}
|
||||
|
||||
await setBucketPolicy();
|
||||
|
||||
logger.info(`S3 Bucket ${bucketName} - ON`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Auth, ConfigService, ProviderSession } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import axios from 'axios';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
type ResponseSuccess = { status: number; data?: any };
|
||||
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
|
||||
@ -36,7 +36,7 @@ export class ProviderFiles {
|
||||
} catch (error) {
|
||||
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
|
||||
const pid = process.pid;
|
||||
execFileSync('kill', ['-9', `${pid}`]);
|
||||
execSync(`kill -9 ${pid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
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();
|
||||
}
|
@ -46,19 +46,14 @@ export class ChatRouter extends RouterBroker {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<WhatsAppNumberDto>({
|
||||
request: req,
|
||||
schema: whatsappNumberSchema,
|
||||
ClassRef: WhatsAppNumberDto,
|
||||
execute: (instance, data) => chatController.whatsappNumber(instance, data),
|
||||
});
|
||||
const response = await this.dataValidate<WhatsAppNumberDto>({
|
||||
request: req,
|
||||
schema: whatsappNumberSchema,
|
||||
ClassRef: WhatsAppNumberDto,
|
||||
execute: (instance, data) => chatController.whatsappNumber(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return res.status(HttpStatus.BAD_REQUEST).json(error);
|
||||
}
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ReadMessageDto>({
|
||||
@ -212,6 +207,7 @@ export class ChatRouter extends RouterBroker {
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
|
||||
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ProfileNameDto>({
|
||||
request: req,
|
||||
|
@ -6,13 +6,11 @@ import { ChatbotRouter } from '@api/integrations/chatbot/chatbot.router';
|
||||
import { EventRouter } from '@api/integrations/event/event.router';
|
||||
import { StorageRouter } from '@api/integrations/storage/storage.router';
|
||||
import { configService } from '@config/env.config';
|
||||
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
|
||||
import { Router } from 'express';
|
||||
import fs from 'fs';
|
||||
import mimeTypes from 'mime-types';
|
||||
import path from 'path';
|
||||
|
||||
import { BusinessRouter } from './business.router';
|
||||
import { CallRouter } from './call.router';
|
||||
import { ChatRouter } from './chat.router';
|
||||
import { GroupRouter } from './group.router';
|
||||
@ -61,7 +59,7 @@ router.get('/assets/*', (req, res) => {
|
||||
router
|
||||
.use((req, res, next) => telemetry.collectTelemetry(req, res, next))
|
||||
|
||||
.get('/', async (req, res) => {
|
||||
.get('/', (req, res) => {
|
||||
res.status(HttpStatus.OK).json({
|
||||
status: HttpStatus.OK,
|
||||
message: 'Welcome to the Evolution API, it is working!',
|
||||
@ -69,8 +67,6 @@ router
|
||||
clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME,
|
||||
manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined,
|
||||
documentation: `https://doc.evolution-api.com`,
|
||||
whatsappWebVersion:
|
||||
process.env.CONFIG_SESSION_PHONE_VERSION || (await fetchLatestWaWebVersion({})).version.join('.'),
|
||||
});
|
||||
})
|
||||
.post('/verify-creds', authGuard['apikey'], async (req, res) => {
|
||||
@ -86,7 +82,6 @@ router
|
||||
.use('/message', new MessageRouter(...guards).router)
|
||||
.use('/call', new CallRouter(...guards).router)
|
||||
.use('/chat', new ChatRouter(...guards).router)
|
||||
.use('/business', new BusinessRouter(...guards).router)
|
||||
.use('/group', new GroupRouter(...guards).router)
|
||||
.use('/template', new TemplateRouter(configService, ...guards).router)
|
||||
.use('/settings', new SettingsRouter(...guards).router)
|
||||
|
@ -15,6 +15,7 @@ export class InstanceRouter extends RouterBroker {
|
||||
super();
|
||||
this.router
|
||||
.post('/create', ...guards, async (req, res) => {
|
||||
console.log('create instance', req.body);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
|
@ -3,7 +3,6 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
|
||||
import { eventEmitter } from '@config/event.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
import { BusinessController } from './controllers/business.controller';
|
||||
import { CallController } from './controllers/call.controller';
|
||||
import { ChatController } from './controllers/chat.controller';
|
||||
import { GroupController } from './controllers/group.controller';
|
||||
@ -22,14 +21,10 @@ import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/
|
||||
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
|
||||
import { DifyService } from './integrations/chatbot/dify/services/dify.service';
|
||||
import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller';
|
||||
import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service';
|
||||
import { EvolutionBotController } from './integrations/chatbot/evolutionBot/controllers/evolutionBot.controller';
|
||||
import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service';
|
||||
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
|
||||
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
|
||||
import { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
|
||||
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
|
||||
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
|
||||
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
|
||||
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
|
||||
@ -103,7 +98,6 @@ export const instanceController = new InstanceController(
|
||||
export const sendMessageController = new SendMessageController(waMonitor);
|
||||
export const callController = new CallController(waMonitor);
|
||||
export const chatController = new ChatController(waMonitor);
|
||||
export const businessController = new BusinessController(waMonitor);
|
||||
export const groupController = new GroupController(waMonitor);
|
||||
export const labelController = new LabelController(waMonitor);
|
||||
|
||||
@ -115,27 +109,20 @@ export const channelController = new ChannelController(prismaRepository, waMonit
|
||||
export const evolutionController = new EvolutionController(prismaRepository, waMonitor);
|
||||
export const metaController = new MetaController(prismaRepository, waMonitor);
|
||||
export const baileysController = new BaileysController(waMonitor);
|
||||
|
||||
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
|
||||
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
|
||||
|
||||
// chatbots
|
||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
|
||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
|
||||
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
|
||||
|
||||
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
|
||||
const openaiService = new OpenaiService(waMonitor, configService, prismaRepository);
|
||||
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
|
||||
|
||||
const difyService = new DifyService(waMonitor, configService, prismaRepository);
|
||||
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
|
||||
|
||||
const evolutionBotService = new EvolutionBotService(waMonitor, prismaRepository, configService, openaiService);
|
||||
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository);
|
||||
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
|
||||
|
||||
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
|
||||
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository);
|
||||
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');
|
||||
|
@ -45,11 +45,11 @@ export class ChannelStartupService {
|
||||
this.chatwootCache,
|
||||
);
|
||||
|
||||
public openaiService = new OpenaiService(waMonitor, this.prismaRepository, this.configService);
|
||||
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository);
|
||||
|
||||
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
|
||||
public openaiService = new OpenaiService(waMonitor, this.configService, this.prismaRepository);
|
||||
|
||||
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
|
||||
public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository);
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceName);
|
||||
@ -503,29 +503,8 @@ export class ChannelStartupService {
|
||||
where['remoteJid'] = remoteJid;
|
||||
}
|
||||
|
||||
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
||||
return await this.prismaRepository.contact.findMany({
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -677,14 +656,6 @@ export class ChannelStartupService {
|
||||
}
|
||||
|
||||
public async fetchStatusMessage(query: any) {
|
||||
if (!query?.offset) {
|
||||
query.offset = 50;
|
||||
}
|
||||
|
||||
if (!query?.page) {
|
||||
query.page = 1;
|
||||
}
|
||||
|
||||
return await this.prismaRepository.messageUpdate.findMany({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
@ -714,100 +685,88 @@ export class ChannelStartupService {
|
||||
const timestampFilter =
|
||||
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
|
||||
? Prisma.sql`
|
||||
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
|
||||
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
|
||||
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
|
||||
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
|
||||
: Prisma.sql``;
|
||||
|
||||
const limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``;
|
||||
const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : Prisma.sql``;
|
||||
|
||||
const results = await this.prismaRepository.$queryRaw`
|
||||
WITH rankedMessages AS (
|
||||
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
|
||||
"Contact"."id" as "contactId",
|
||||
"Message"."key"->>'remoteJid' as "remoteJid",
|
||||
CASE
|
||||
WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName")
|
||||
ELSE COALESCE("Contact"."pushName", "Message"."pushName")
|
||||
END as "pushName",
|
||||
"Contact"."profilePicUrl",
|
||||
COALESCE(
|
||||
to_timestamp("Message"."messageTimestamp"::double precision),
|
||||
"Contact"."updatedAt"
|
||||
) as "updatedAt",
|
||||
"Chat"."name" as "pushName",
|
||||
"Chat"."createdAt" as "windowStart",
|
||||
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
|
||||
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
|
||||
"Message"."id" AS lastMessageId,
|
||||
"Message"."key" AS lastMessage_key,
|
||||
CASE
|
||||
WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você'
|
||||
ELSE "Message"."pushName"
|
||||
END AS lastMessagePushName,
|
||||
"Message"."participant" AS lastMessageParticipant,
|
||||
"Message"."messageType" AS lastMessageMessageType,
|
||||
"Message"."message" AS lastMessageMessage,
|
||||
"Message"."contextInfo" AS lastMessageContextInfo,
|
||||
"Message"."source" AS lastMessageSource,
|
||||
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
|
||||
"Message"."instanceId" AS lastMessageInstanceId,
|
||||
"Message"."sessionId" AS lastMessageSessionId,
|
||||
"Message"."status" AS lastMessageStatus
|
||||
FROM "Message"
|
||||
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
|
||||
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
|
||||
WHERE "Message"."instanceId" = ${this.instanceId}
|
||||
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
|
||||
${timestampFilter}
|
||||
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
|
||||
)
|
||||
SELECT * FROM rankedMessages
|
||||
ORDER BY "updatedAt" DESC NULLS LAST
|
||||
${limit}
|
||||
${offset};
|
||||
WITH rankedMessages AS (
|
||||
SELECT DISTINCT ON ("Contact"."remoteJid")
|
||||
"Contact"."id",
|
||||
"Contact"."remoteJid",
|
||||
"Contact"."pushName",
|
||||
"Contact"."profilePicUrl",
|
||||
COALESCE(
|
||||
to_timestamp("Message"."messageTimestamp"::double precision),
|
||||
"Contact"."updatedAt"
|
||||
) as "updatedAt",
|
||||
"Chat"."createdAt" as "windowStart",
|
||||
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
|
||||
CASE
|
||||
WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true
|
||||
ELSE false
|
||||
END as "windowActive",
|
||||
"Message"."id" AS lastMessageId,
|
||||
"Message"."key" AS lastMessage_key,
|
||||
"Message"."pushName" AS lastMessagePushName,
|
||||
"Message"."participant" AS lastMessageParticipant,
|
||||
"Message"."messageType" AS lastMessageMessageType,
|
||||
"Message"."message" AS lastMessageMessage,
|
||||
"Message"."contextInfo" AS lastMessageContextInfo,
|
||||
"Message"."source" AS lastMessageSource,
|
||||
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
|
||||
"Message"."instanceId" AS lastMessageInstanceId,
|
||||
"Message"."sessionId" AS lastMessageSessionId,
|
||||
"Message"."status" AS lastMessageStatus
|
||||
FROM "Contact"
|
||||
INNER JOIN "Message" ON "Message"."key"->>'remoteJid' = "Contact"."remoteJid"
|
||||
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Contact"."remoteJid"
|
||||
AND "Chat"."instanceId" = "Contact"."instanceId"
|
||||
WHERE
|
||||
"Contact"."instanceId" = ${this.instanceId}
|
||||
AND "Message"."instanceId" = ${this.instanceId}
|
||||
${remoteJid ? Prisma.sql`AND "Contact"."remoteJid" = ${remoteJid}` : Prisma.sql``}
|
||||
${timestampFilter}
|
||||
ORDER BY
|
||||
"Contact"."remoteJid",
|
||||
"Message"."messageTimestamp" DESC
|
||||
)
|
||||
SELECT * FROM rankedMessages
|
||||
ORDER BY "updatedAt" DESC NULLS LAST;
|
||||
`;
|
||||
|
||||
if (results && isArray(results) && results.length > 0) {
|
||||
const mappedResults = results.map((contact) => {
|
||||
const lastMessage = contact.lastmessageid
|
||||
const lastMessage = contact.lastMessageId
|
||||
? {
|
||||
id: contact.lastmessageid,
|
||||
key: contact.lastmessage_key,
|
||||
pushName: contact.lastmessagepushname,
|
||||
participant: contact.lastmessageparticipant,
|
||||
messageType: contact.lastmessagemessagetype,
|
||||
message: contact.lastmessagemessage,
|
||||
contextInfo: contact.lastmessagecontextinfo,
|
||||
source: contact.lastmessagesource,
|
||||
messageTimestamp: contact.lastmessagemessagetimestamp,
|
||||
instanceId: contact.lastmessageinstanceid,
|
||||
sessionId: contact.lastmessagesessionid,
|
||||
status: contact.lastmessagestatus,
|
||||
id: contact.lastMessageId,
|
||||
key: contact.lastMessageKey,
|
||||
pushName: contact.lastMessagePushName,
|
||||
participant: contact.lastMessageParticipant,
|
||||
messageType: contact.lastMessageMessageType,
|
||||
message: contact.lastMessageMessage,
|
||||
contextInfo: contact.lastMessageContextInfo,
|
||||
source: contact.lastMessageSource,
|
||||
messageTimestamp: contact.lastMessageMessageTimestamp,
|
||||
instanceId: contact.lastMessageInstanceId,
|
||||
sessionId: contact.lastMessageSessionId,
|
||||
status: contact.lastMessageStatus,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: contact.contactid || null,
|
||||
remoteJid: contact.remotejid,
|
||||
pushName: contact.pushname,
|
||||
profilePicUrl: contact.profilepicurl,
|
||||
updatedAt: contact.updatedat,
|
||||
windowStart: contact.windowstart,
|
||||
windowExpires: contact.windowexpires,
|
||||
windowActive: contact.windowactive,
|
||||
id: contact.id,
|
||||
remoteJid: contact.remoteJid,
|
||||
pushName: contact.pushName,
|
||||
profilePicUrl: contact.profilePicUrl,
|
||||
updatedAt: contact.updatedAt,
|
||||
windowStart: contact.windowStart,
|
||||
windowExpires: contact.windowExpires,
|
||||
windowActive: contact.windowActive,
|
||||
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
|
||||
unreadCount: 0,
|
||||
isSaved: !!contact.contactid,
|
||||
};
|
||||
});
|
||||
|
||||
if (query?.take && query?.skip) {
|
||||
const skip = query.skip || 0;
|
||||
const take = query.take || 20;
|
||||
return mappedResults.slice(skip, skip + take);
|
||||
}
|
||||
|
||||
return mappedResults;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
|
||||
import { NotFoundException } from '@exceptions';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { execSync } from 'child_process';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
@ -91,7 +91,6 @@ export class WAMonitoringService {
|
||||
Chatwoot: true,
|
||||
Proxy: true,
|
||||
Rabbitmq: true,
|
||||
Nats: true,
|
||||
Sqs: true,
|
||||
Websocket: true,
|
||||
Setting: true,
|
||||
@ -169,8 +168,7 @@ export class WAMonitoringService {
|
||||
|
||||
public async cleaningStoreData(instanceName: string) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
|
||||
execFileSync('rm', ['-rf', instancePath]);
|
||||
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({
|
||||
@ -192,7 +190,6 @@ export class WAMonitoringService {
|
||||
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });
|
||||
|
@ -15,7 +15,6 @@ export enum Events {
|
||||
MESSAGES_UPDATE = 'messages.update',
|
||||
MESSAGES_DELETE = 'messages.delete',
|
||||
SEND_MESSAGE = 'send.message',
|
||||
SEND_MESSAGE_UPDATE = 'send.message.update',
|
||||
CONTACTS_SET = 'contacts.set',
|
||||
CONTACTS_UPSERT = 'contacts.upsert',
|
||||
CONTACTS_UPDATE = 'contacts.update',
|
||||
|
@ -72,7 +72,6 @@ export type EventsRabbitmq = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -95,20 +94,10 @@ export type EventsRabbitmq = {
|
||||
export type Rabbitmq = {
|
||||
ENABLED: boolean;
|
||||
URI: string;
|
||||
FRAME_MAX: number;
|
||||
EXCHANGE_NAME: string;
|
||||
GLOBAL_ENABLED: boolean;
|
||||
EVENTS: EventsRabbitmq;
|
||||
PREFIX_KEY?: string;
|
||||
};
|
||||
|
||||
export type Nats = {
|
||||
ENABLED: boolean;
|
||||
URI: string;
|
||||
EXCHANGE_NAME: string;
|
||||
GLOBAL_ENABLED: boolean;
|
||||
EVENTS: EventsRabbitmq;
|
||||
PREFIX_KEY?: string;
|
||||
PREFIX_KEY: string;
|
||||
};
|
||||
|
||||
export type Sqs = {
|
||||
@ -142,7 +131,6 @@ export type EventsWebhook = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -175,7 +163,6 @@ export type EventsPusher = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -233,21 +220,7 @@ export type CacheConfLocal = {
|
||||
TTL: number;
|
||||
};
|
||||
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
|
||||
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 Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
|
||||
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
||||
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
|
||||
export type QrCode = { LIMIT: number; COLOR: string };
|
||||
@ -268,9 +241,6 @@ export type Chatwoot = {
|
||||
};
|
||||
export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
|
||||
export type Dify = { ENABLED: boolean };
|
||||
export type N8n = { ENABLED: boolean };
|
||||
export type Evoai = { ENABLED: boolean };
|
||||
export type Flowise = { ENABLED: boolean };
|
||||
|
||||
export type S3 = {
|
||||
ACCESS_KEY: string;
|
||||
@ -281,7 +251,6 @@ export type S3 = {
|
||||
PORT?: number;
|
||||
USE_SSL?: boolean;
|
||||
REGION?: string;
|
||||
SKIP_POLICY?: boolean;
|
||||
};
|
||||
|
||||
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
|
||||
@ -294,7 +263,6 @@ export interface Env {
|
||||
PROVIDER: ProviderSession;
|
||||
DATABASE: Database;
|
||||
RABBITMQ: Rabbitmq;
|
||||
NATS: Nats;
|
||||
SQS: Sqs;
|
||||
WEBSOCKET: Websocket;
|
||||
WA_BUSINESS: WaBusiness;
|
||||
@ -310,9 +278,6 @@ export interface Env {
|
||||
CHATWOOT: Chatwoot;
|
||||
OPENAI: Openai;
|
||||
DIFY: Dify;
|
||||
N8N: N8n;
|
||||
EVOAI: Evoai;
|
||||
FLOWISE: Flowise;
|
||||
CACHE: CacheConf;
|
||||
S3?: S3;
|
||||
AUTHENTICATION: Auth;
|
||||
@ -391,10 +356,9 @@ export class ConfigService {
|
||||
RABBITMQ: {
|
||||
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
|
||||
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
|
||||
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY,
|
||||
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || 'evolution',
|
||||
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
|
||||
URI: process.env.RABBITMQ_URI || '',
|
||||
FRAME_MAX: Number.parseInt(process.env.RABBITMQ_FRAME_MAX) || 8192,
|
||||
EVENTS: {
|
||||
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
|
||||
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',
|
||||
@ -406,7 +370,6 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -426,43 +389,6 @@ export class ConfigService {
|
||||
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
|
||||
},
|
||||
},
|
||||
NATS: {
|
||||
ENABLED: process.env?.NATS_ENABLED === 'true',
|
||||
GLOBAL_ENABLED: process.env?.NATS_GLOBAL_ENABLED === 'true',
|
||||
PREFIX_KEY: process.env?.NATS_PREFIX_KEY,
|
||||
EXCHANGE_NAME: process.env?.NATS_EXCHANGE_NAME || 'evolution_exchange',
|
||||
URI: process.env.NATS_URI || '',
|
||||
EVENTS: {
|
||||
APPLICATION_STARTUP: process.env?.NATS_EVENTS_APPLICATION_STARTUP === 'true',
|
||||
INSTANCE_CREATE: process.env?.NATS_EVENTS_INSTANCE_CREATE === 'true',
|
||||
INSTANCE_DELETE: process.env?.NATS_EVENTS_INSTANCE_DELETE === 'true',
|
||||
QRCODE_UPDATED: process.env?.NATS_EVENTS_QRCODE_UPDATED === 'true',
|
||||
MESSAGES_SET: process.env?.NATS_EVENTS_MESSAGES_SET === 'true',
|
||||
MESSAGES_UPSERT: process.env?.NATS_EVENTS_MESSAGES_UPSERT === 'true',
|
||||
MESSAGES_EDITED: process.env?.NATS_EVENTS_MESSAGES_EDITED === 'true',
|
||||
MESSAGES_UPDATE: process.env?.NATS_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.NATS_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.NATS_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.NATS_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.NATS_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.NATS_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.NATS_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
PRESENCE_UPDATE: process.env?.NATS_EVENTS_PRESENCE_UPDATE === 'true',
|
||||
CHATS_SET: process.env?.NATS_EVENTS_CHATS_SET === 'true',
|
||||
CHATS_UPDATE: process.env?.NATS_EVENTS_CHATS_UPDATE === 'true',
|
||||
CHATS_UPSERT: process.env?.NATS_EVENTS_CHATS_UPSERT === 'true',
|
||||
CHATS_DELETE: process.env?.NATS_EVENTS_CHATS_DELETE === 'true',
|
||||
CONNECTION_UPDATE: process.env?.NATS_EVENTS_CONNECTION_UPDATE === 'true',
|
||||
LABELS_EDIT: process.env?.NATS_EVENTS_LABELS_EDIT === 'true',
|
||||
LABELS_ASSOCIATION: process.env?.NATS_EVENTS_LABELS_ASSOCIATION === 'true',
|
||||
GROUPS_UPSERT: process.env?.NATS_EVENTS_GROUPS_UPSERT === 'true',
|
||||
GROUP_UPDATE: process.env?.NATS_EVENTS_GROUPS_UPDATE === 'true',
|
||||
GROUP_PARTICIPANTS_UPDATE: process.env?.NATS_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
|
||||
CALL: process.env?.NATS_EVENTS_CALL === 'true',
|
||||
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
|
||||
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
|
||||
},
|
||||
},
|
||||
SQS: {
|
||||
ENABLED: process.env?.SQS_ENABLED === 'true',
|
||||
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '',
|
||||
@ -495,7 +421,6 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.PUSHER_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -552,7 +477,6 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -573,19 +497,6 @@ export class ConfigService {
|
||||
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
|
||||
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
|
||||
},
|
||||
REQUEST: {
|
||||
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
|
||||
},
|
||||
RETRY: {
|
||||
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
|
||||
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
|
||||
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
|
||||
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
|
||||
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
|
||||
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [
|
||||
400, 401, 403, 404, 422,
|
||||
],
|
||||
},
|
||||
},
|
||||
CONFIG_SESSION_PHONE: {
|
||||
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
||||
@ -622,15 +533,6 @@ export class ConfigService {
|
||||
DIFY: {
|
||||
ENABLED: process.env?.DIFY_ENABLED === 'true',
|
||||
},
|
||||
N8N: {
|
||||
ENABLED: process.env?.N8N_ENABLED === 'true',
|
||||
},
|
||||
EVOAI: {
|
||||
ENABLED: process.env?.EVOAI_ENABLED === 'true',
|
||||
},
|
||||
FLOWISE: {
|
||||
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
|
||||
},
|
||||
CACHE: {
|
||||
REDIS: {
|
||||
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
|
||||
@ -653,7 +555,6 @@ export class ConfigService {
|
||||
PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
|
||||
USE_SSL: process.env?.S3_USE_SSL === 'true',
|
||||
REGION: process.env?.S3_REGION,
|
||||
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
|
||||
},
|
||||
AUTHENTICATION: {
|
||||
API_KEY: {
|
||||
|
10
src/main.ts
10
src/main.ts
@ -128,15 +128,7 @@ async function bootstrap() {
|
||||
const httpServer = configService.get<HttpServer>('SERVER');
|
||||
|
||||
ServerUP.app = app;
|
||||
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];
|
||||
}
|
||||
const server = ServerUP[httpServer.TYPE];
|
||||
|
||||
eventManager.init(server);
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
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
Loading…
Reference in New Issue
Block a user