mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-12 22:54:49 -06:00
Merge branch 'develop' into patch-1
This commit is contained in:
commit
bd19fff264
@ -1,5 +1,7 @@
|
||||
.git
|
||||
*Dockerfile*
|
||||
*docker-compose*
|
||||
package-lock.json
|
||||
.env
|
||||
node_modules
|
||||
dist
|
24
.env.example
24
.env.example
@ -3,6 +3,9 @@ SERVER_PORT=8080
|
||||
# Server URL - Set your application url
|
||||
SERVER_URL=http://localhost:8080
|
||||
|
||||
SSL_CONF_PRIVKEY=/path/to/cert.key
|
||||
SSL_CONF_FULLCHAIN=/path/to/cert.crt
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
@ -47,6 +50,7 @@ DATABASE_DELETE_MESSAGE=true
|
||||
RABBITMQ_ENABLED=false
|
||||
RABBITMQ_URI=amqp://localhost
|
||||
RABBITMQ_EXCHANGE_NAME=evolution
|
||||
RABBITMQ_FRAME_MAX=8192
|
||||
# Global events - By enabling this variable, events from all instances are sent in the same event queue.
|
||||
RABBITMQ_GLOBAL_ENABLED=false
|
||||
# Prefix key to queue name
|
||||
@ -62,6 +66,7 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
|
||||
RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@ -108,6 +113,7 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
|
||||
PUSHER_EVENTS_MESSAGES_UPDATE=true
|
||||
PUSHER_EVENTS_MESSAGES_DELETE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
PUSHER_EVENTS_CONTACTS_SET=true
|
||||
PUSHER_EVENTS_CONTACTS_UPSERT=true
|
||||
PUSHER_EVENTS_CONTACTS_UPDATE=true
|
||||
@ -149,6 +155,7 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
@ -173,6 +180,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||
WEBHOOK_EVENTS_ERRORS=false
|
||||
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
|
||||
WEBHOOK_REQUEST_TIMEOUT_MS=60000
|
||||
WEBHOOK_RETRY_MAX_ATTEMPTS=10
|
||||
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
|
||||
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
|
||||
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
|
||||
WEBHOOK_RETRY_JITTER_FACTOR=0.2
|
||||
# Comma separated list of HTTP status codes that should not trigger retries
|
||||
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
|
||||
|
||||
# Name that will be displayed on smartphone connection
|
||||
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
||||
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
||||
@ -210,6 +226,12 @@ OPENAI_ENABLED=false
|
||||
# Dify - Environment variables
|
||||
DIFY_ENABLED=false
|
||||
|
||||
# n8n - Environment variables
|
||||
N8N_ENABLED=false
|
||||
|
||||
# EvoAI - Environment variables
|
||||
EVOAI_ENABLED=false
|
||||
|
||||
# Cache - Environment variables
|
||||
# Redis Cache enabled
|
||||
CACHE_REDIS_ENABLED=true
|
||||
@ -266,4 +288,4 @@ LANGUAGE=en
|
||||
# PROXY_PORT=80
|
||||
# PROXY_PROTOCOL=http
|
||||
# PROXY_USERNAME=
|
||||
# PROXY_PASSWORD=
|
||||
# PROXY_PASSWORD=
|
||||
|
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: atendai/evolution-api
|
||||
images: evoapicloud/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: atendai/evolution-api
|
||||
images: evoapicloud/evolution-api
|
||||
tags: homolog
|
||||
|
||||
- name: Set up QEMU
|
||||
|
@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
images: evoapicloud/evolution-api
|
||||
tags: latest
|
||||
|
||||
- name: Set up QEMU
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
.cursor*
|
||||
|
||||
/Docker/.env
|
||||
|
||||
.vscode
|
||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@ -1,3 +1,32 @@
|
||||
# 2.3.0 (develop)
|
||||
|
||||
### 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.17
|
||||
* 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
|
||||
|
||||
### Security
|
||||
|
||||
* Change execSync to execFileSync
|
||||
* Enhance WebSocket authentication and connection handling
|
||||
|
||||
# 2.2.3 (2025-02-03 11:52)
|
||||
|
||||
### Fixed
|
||||
|
@ -2,7 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
evolution_v2:
|
||||
image: atendai/evolution-api:v2.2.3
|
||||
image: atendai/evolution-api:v2.2.3
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
@ -34,6 +34,7 @@ services:
|
||||
- RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
- RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@ -71,6 +72,7 @@ services:
|
||||
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
- WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
|
@ -1,11 +1,11 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && \
|
||||
apk add git ffmpeg wget curl bash openssl
|
||||
apk add --no-cache git ffmpeg wget curl bash openssl
|
||||
|
||||
LABEL version="2.2.3" description="Api to control whatsapp features through http requests."
|
||||
LABEL version="2.3.0" description="Api to control whatsapp features through http requests."
|
||||
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
|
||||
LABEL contact="contato@atendai.com"
|
||||
LABEL contact="contato@evolution-api.com"
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
|
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@atendai.com to inquire about licensing matters.
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
|
||||
2. As a contributor, you should agree that:
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[]
|
||||
[](https://evolution-api.com/whatsapp)
|
||||
[](https://evolution-api.com/discord)
|
||||
[](https://evolution-api.com/postman)
|
||||
@ -87,6 +88,7 @@ https://github.com/sponsors/EvolutionAPI
|
||||
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
|
||||
|
||||
- [Promovaweb](https://www.youtube.com/@promovaweb)
|
||||
- [Sandeco](https://www.youtube.com/@canalsandeco)
|
||||
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
|
||||
- [Francis MNO](https://www.youtube.com/@FrancisMNO)
|
||||
- [Pablo Cabral](https://youtube.com/@pablocabral)
|
||||
@ -111,7 +113,7 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
|
||||
|
||||
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
|
||||
|
||||
Please contact contato@atendai.com to inquire about licensing matters.
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: atendai/evolution-api:homolog
|
||||
image: evoapicloud/evolution-api:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
- redis
|
||||
|
381
manager/dist/assets/index-CFAZX6IV.js
vendored
381
manager/dist/assets/index-CFAZX6IV.js
vendored
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-CXH2BdD4.css
vendored
Normal file
1
manager/dist/assets/index-CXH2BdD4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
381
manager/dist/assets/index-D-oOjDYe.js
vendored
Normal file
381
manager/dist/assets/index-D-oOjDYe.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DNOCacL_.css
vendored
1
manager/dist/assets/index-DNOCacL_.css
vendored
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="/assets/images/evolution-logo.png" />
|
||||
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Evolution Manager</title>
|
||||
<script type="module" crossorigin src="/assets/index-CFAZX6IV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
|
||||
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
3233
package-lock.json
generated
3233
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.0",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
@ -18,6 +18,7 @@
|
||||
"db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
|
||||
"db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"",
|
||||
"db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\""
|
||||
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -41,7 +42,7 @@
|
||||
],
|
||||
"author": {
|
||||
"name": "Davidson Gomes",
|
||||
"email": "contato@atendai.com"
|
||||
"email": "contato@evolution-api.com"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@ -75,6 +76,7 @@
|
||||
"jimp": "^0.16.13",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"link-preview-js": "^3.0.13",
|
||||
"long": "^5.2.3",
|
||||
"mediainfo.js": "^0.3.4",
|
||||
@ -82,6 +84,7 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"minio": "^8.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nats": "^2.29.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.77.3",
|
||||
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
*/
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Setting`
|
||||
ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100);
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- A unique constraint covering the columns `[instanceId,remoteJid]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Setting` ADD COLUMN `wavoipToken` VARCHAR(100) NULL,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Chat_instanceId_remoteJid_key` ON `Chat`(`instanceId`, `remoteJid`);
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
|
||||
-- AlterTable
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Setting'
|
||||
AND column_name = 'wavoipToken'
|
||||
);
|
||||
|
||||
SET @sql := IF(@column_exists = 0,
|
||||
'ALTER TABLE Setting ADD COLUMN wavoipToken VARCHAR(100);',
|
||||
'SELECT "Column already exists";'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE Chat ADD CONSTRAINT unique_remote_instance UNIQUE (remoteJid, instanceId);
|
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
@ -86,6 +86,7 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@ -99,12 +100,13 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
integrationSessions IntegrationSession[]
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@ -116,18 +118,19 @@ model Session {
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels Json? @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels Json? @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@unique([instanceId, remoteJid])
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
@@unique([instanceId, remoteJid])
|
||||
}
|
||||
|
||||
model Contact {
|
||||
@ -170,6 +173,7 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -185,6 +189,7 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@ -201,6 +206,7 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -269,6 +275,7 @@ model Setting {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -282,6 +289,16 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
events Json @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
@ -382,7 +399,7 @@ model IntegrationSession {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @unique @db.VarChar(500)
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
@ -627,3 +644,100 @@ model IsOnWhatsapp {
|
||||
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Nats" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"events" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Nats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Nats_instanceId_key" ON "Nats"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Nats" ADD CONSTRAINT "Nats_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,62 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "N8n" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"description" VARCHAR(255),
|
||||
"webhookUrl" VARCHAR(255),
|
||||
"basicAuthUser" VARCHAR(255),
|
||||
"basicAuthPass" VARCHAR(255),
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"triggerType" "TriggerType",
|
||||
"triggerOperator" "TriggerOperator",
|
||||
"triggerValue" TEXT,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "N8n_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "N8nSetting" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"n8nIdFallback" VARCHAR(100),
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "N8nSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "N8nSetting_instanceId_key" ON "N8nSetting"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8n" ADD CONSTRAINT "N8n_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_n8nIdFallback_fkey" FOREIGN KEY ("n8nIdFallback") REFERENCES "N8n"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Evoai" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"description" VARCHAR(255),
|
||||
"agentUrl" VARCHAR(255),
|
||||
"apiKey" VARCHAR(255),
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"triggerType" "TriggerType",
|
||||
"triggerOperator" "TriggerOperator",
|
||||
"triggerValue" TEXT,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Evoai_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EvoaiSetting" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"evoaiIdFallback" VARCHAR(100),
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "EvoaiSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EvoaiSetting_instanceId_key" ON "EvoaiSetting"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Evoai" ADD CONSTRAINT "Evoai_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_evoaiIdFallback_fkey" FOREIGN KEY ("evoaiIdFallback") REFERENCES "Evoai"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Media_fileName_key";
|
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
@ -86,6 +86,7 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@ -99,12 +100,16 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
integrationSessions IntegrationSession[]
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
@ -125,6 +130,7 @@ model Chat {
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
@ -168,6 +174,7 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -183,6 +190,7 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@ -199,6 +207,7 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -269,6 +278,7 @@ model Setting {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@ -282,6 +292,16 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
@ -363,7 +383,7 @@ model TypebotSetting {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @unique @db.VarChar(500)
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Date
|
||||
@ -627,3 +647,100 @@ model IsOnWhatsapp {
|
||||
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
|
||||
}
|
15
src/api/controllers/business.controller.ts
Normal file
15
src/api/controllers/business.controller.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
|
||||
export class BusinessController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
|
||||
}
|
||||
|
||||
public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
|
||||
}
|
||||
}
|
@ -170,6 +170,9 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
@ -258,6 +261,9 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
|
@ -18,6 +18,14 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
|
||||
function isEmoji(str: string) {
|
||||
if (str === '') return true;
|
||||
|
||||
const emojiRegex =
|
||||
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
|
||||
return emojiRegex.test(str);
|
||||
}
|
||||
|
||||
export class SendMessageController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
@ -81,8 +89,8 @@ export class SendMessageController {
|
||||
}
|
||||
|
||||
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
|
||||
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
|
||||
throw new BadRequestException('"reaction" must be an emoji');
|
||||
if (!isEmoji(data.reaction)) {
|
||||
throw new BadRequestException('Reaction must be a single emoji or empty string');
|
||||
}
|
||||
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
|
||||
}
|
||||
|
14
src/api/dto/business.dto.ts
Normal file
14
src/api/dto/business.dto.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export class NumberDto {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class getCatalogDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export class getCollectionsDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
}
|
@ -44,6 +44,7 @@ export class Metadata {
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
encoding?: boolean;
|
||||
notConvertSticker?: boolean;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
|
@ -165,11 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
openAiDefaultSettings.speechToText &&
|
||||
received?.message?.audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
received,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(received, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,17 +192,77 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
private messageTextJson(received: any) {
|
||||
let content: any;
|
||||
// Verificar que received y received.messages existen
|
||||
if (!received || !received.messages || received.messages.length === 0) {
|
||||
this.logger.error('Error: received object or messages array is undefined or empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = received.messages[0];
|
||||
let content: any;
|
||||
|
||||
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
|
||||
if (!message.text) {
|
||||
// Si no hay texto, manejamos diferente según el tipo de mensaje
|
||||
if (message.type === 'sticker') {
|
||||
content = { stickerMessage: {} };
|
||||
} else if (message.type === 'location') {
|
||||
content = {
|
||||
locationMessage: {
|
||||
degreesLatitude: message.location?.latitude,
|
||||
degreesLongitude: message.location?.longitude,
|
||||
name: message.location?.name,
|
||||
address: message.location?.address,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
|
||||
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
|
||||
content = { [message.type + 'Message']: message[message.type] || {} };
|
||||
}
|
||||
|
||||
// Añadir contexto si existe
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Si el mensaje tiene texto, procesamos normalmente
|
||||
if (!received.metadata || !received.metadata.phone_number_id) {
|
||||
this.logger.error('Error: metadata or phone_number_id is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.from === received.metadata.phone_number_id) {
|
||||
content = {
|
||||
extendedTextMessage: { text: message.text.body },
|
||||
};
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
} else {
|
||||
content = { conversation: message.text.body };
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private messageLocationJson(received: any) {
|
||||
const message = received.messages[0];
|
||||
let content: any = {
|
||||
locationMessage: {
|
||||
degreesLatitude: message.location.latitude,
|
||||
degreesLongitude: message.location.longitude,
|
||||
name: message.location?.name,
|
||||
address: message.location?.address,
|
||||
},
|
||||
};
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
return content;
|
||||
}
|
||||
|
||||
@ -283,6 +343,12 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
case 'template':
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
case 'location':
|
||||
messageType = 'locationMessage';
|
||||
break;
|
||||
case 'sticker':
|
||||
messageType = 'stickerMessage';
|
||||
break;
|
||||
default:
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
@ -299,12 +365,28 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
if (received.contacts) pushName = received.contacts[0].profile.name;
|
||||
|
||||
if (received.messages) {
|
||||
const message = received.messages[0]; // Añadir esta línea para definir message
|
||||
|
||||
const key = {
|
||||
id: received.messages[0].id,
|
||||
id: message.id,
|
||||
remoteJid: this.phoneNumber,
|
||||
fromMe: received.messages[0].from === received.metadata.phone_number_id,
|
||||
fromMe: message.from === received.metadata.phone_number_id,
|
||||
};
|
||||
if (this.isMediaMessage(received?.messages[0])) {
|
||||
|
||||
if (message.type === 'sticker') {
|
||||
this.logger.log('Procesando mensaje de tipo sticker');
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
message: {
|
||||
stickerMessage: message.sticker || {},
|
||||
},
|
||||
messageType: 'stickerMessage',
|
||||
messageTimestamp: parseInt(message.timestamp) as number,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (this.isMediaMessage(message)) {
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
@ -473,16 +555,12 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
openAiDefaultSettings.speechToText &&
|
||||
audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
},
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(openAiDefaultSettings.OpenaiCreds, {
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -511,7 +589,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isMediaMessage(received?.messages[0])) {
|
||||
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
|
||||
await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
@ -714,17 +792,54 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
protected async eventHandler(content: any) {
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
try {
|
||||
// Registro para depuración
|
||||
this.logger.log('Contenido recibido en eventHandler:');
|
||||
this.logger.log(JSON.stringify(content, null, 2));
|
||||
|
||||
this.messageHandle(content, database, settings);
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
|
||||
// Si hay mensajes, verificar primero el tipo
|
||||
if (content.messages && content.messages.length > 0) {
|
||||
const message = content.messages[0];
|
||||
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
|
||||
|
||||
// Verificamos el tipo de mensaje antes de procesarlo
|
||||
if (
|
||||
message.type === 'text' ||
|
||||
message.type === 'image' ||
|
||||
message.type === 'video' ||
|
||||
message.type === 'audio' ||
|
||||
message.type === 'document' ||
|
||||
message.type === 'sticker' ||
|
||||
message.type === 'location' ||
|
||||
message.type === 'contacts' ||
|
||||
message.type === 'interactive' ||
|
||||
message.type === 'button' ||
|
||||
message.type === 'reaction'
|
||||
) {
|
||||
// Procesar el mensaje normalmente
|
||||
this.messageHandle(content, database, settings);
|
||||
} else {
|
||||
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
|
||||
}
|
||||
} else if (content.statuses) {
|
||||
// Procesar actualizaciones de estado
|
||||
this.messageHandle(content, database, settings);
|
||||
} else {
|
||||
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error en eventHandler:');
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
||||
try {
|
||||
let quoted: any;
|
||||
let webhookUrl: any;
|
||||
const linkPreview = options?.linkPreview != false ? undefined : false;
|
||||
if (options?.quoted) {
|
||||
const m = options?.quoted;
|
||||
|
||||
@ -792,7 +907,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
text: {
|
||||
body: message['conversation'],
|
||||
preview_url: linkPreview,
|
||||
preview_url: Boolean(options?.linkPreview),
|
||||
},
|
||||
};
|
||||
quoted ? (content.context = { message_id: quoted.id }) : content;
|
||||
@ -808,7 +923,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
[message['mediaType']]: {
|
||||
[message['type']]: message['id'],
|
||||
preview_url: linkPreview,
|
||||
preview_url: Boolean(options?.linkPreview),
|
||||
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
|
||||
caption: message['caption'],
|
||||
},
|
||||
@ -977,7 +1092,6 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
|
||||
private async getIdMedia(mediaMessage: any) {
|
||||
const formData = new FormData();
|
||||
|
||||
const fileStream = createReadStream(mediaMessage.media);
|
||||
|
||||
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getCollectionsDto } from '@api/dto/business.dto';
|
||||
import { OfferCallDto } from '@api/dto/call.dto';
|
||||
import {
|
||||
ArchiveChatDto,
|
||||
@ -54,7 +55,7 @@ import {
|
||||
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
||||
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
||||
import { ProviderFiles } from '@api/provider/sessions';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { PrismaRepository, Query } from '@api/repository/repository.service';
|
||||
import { chatbotController, waMonitor } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { ChannelStartupService } from '@api/services/channel.service';
|
||||
@ -77,7 +78,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
|
||||
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
|
||||
import { Boom } from '@hapi/boom';
|
||||
import { createId as cuid } from '@paralleldrive/cuid2';
|
||||
import { Instance } from '@prisma/client';
|
||||
import { Instance, Message } from '@prisma/client';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
||||
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
||||
@ -91,6 +92,7 @@ import makeWASocket, {
|
||||
BufferedEventData,
|
||||
BufferJSON,
|
||||
CacheStore,
|
||||
CatalogCollection,
|
||||
Chat,
|
||||
ConnectionState,
|
||||
Contact,
|
||||
@ -100,6 +102,7 @@ import makeWASocket, {
|
||||
fetchLatestBaileysVersion,
|
||||
generateWAMessageFromContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
GetCatalogOptions,
|
||||
getContentType,
|
||||
getDevice,
|
||||
GroupMetadata,
|
||||
@ -113,6 +116,7 @@ import makeWASocket, {
|
||||
MiscMessageGenerationOptions,
|
||||
ParticipantAction,
|
||||
prepareWAMessageMedia,
|
||||
Product,
|
||||
proto,
|
||||
UserFacingSocketConfig,
|
||||
WABrowserDescription,
|
||||
@ -130,7 +134,6 @@ import { randomBytes } from 'crypto';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import FormData from 'form-data';
|
||||
import { readFileSync } from 'fs';
|
||||
import Long from 'long';
|
||||
import mimeTypes from 'mime-types';
|
||||
import NodeCache from 'node-cache';
|
||||
@ -226,7 +229,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
private authStateProvider: AuthStateProvider;
|
||||
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
|
||||
private readonly userDevicesCache: CacheStore = new NodeCache();
|
||||
private readonly userDevicesCache: CacheStore = new NodeCache({
|
||||
stdTTL: 300000,
|
||||
useClones: false,
|
||||
});
|
||||
private endSession = false;
|
||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||
|
||||
@ -658,6 +664,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
qrTimeout: 45_000,
|
||||
emitOwnEvents: false,
|
||||
shouldIgnoreJid: (jid) => {
|
||||
if (this.localSettings.syncFullHistory && isJidGroup(jid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid);
|
||||
const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid);
|
||||
const isNewsletter = isJidNewsletter(jid);
|
||||
@ -985,6 +995,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
const contactsMap = new Map();
|
||||
|
||||
for (const contact of contacts) {
|
||||
if (contact.id && (contact.notify || contact.name)) {
|
||||
contactsMap.set(contact.id, {
|
||||
name: contact.name ?? contact.notify,
|
||||
jid: contact.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = [];
|
||||
const chatsRepository = new Set(
|
||||
(
|
||||
@ -1056,6 +1077,15 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!m.pushName && !m.key.fromMe) {
|
||||
const participantJid = m.participant || m.key.participant || m.key.remoteJid;
|
||||
if (participantJid && contactsMap.has(participantJid)) {
|
||||
m.pushName = contactsMap.get(participantJid).name;
|
||||
} else if (participantJid) {
|
||||
m.pushName = participantJid.split('@')[0];
|
||||
}
|
||||
}
|
||||
|
||||
messagesRaw.push(this.prepareMessage(m));
|
||||
}
|
||||
|
||||
@ -1128,38 +1158,54 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) {
|
||||
const editedMessage =
|
||||
received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage;
|
||||
if (editedMessage) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
|
||||
this.chatwootService.eventWhatsapp(
|
||||
'messages.edit',
|
||||
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
||||
editedMessage,
|
||||
);
|
||||
const editedMessage =
|
||||
received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
|
||||
|
||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
||||
if (received.message?.protocolMessage?.editedMessage && editedMessage) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
|
||||
this.chatwootService.eventWhatsapp(
|
||||
'messages.edit',
|
||||
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
||||
editedMessage,
|
||||
);
|
||||
|
||||
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
||||
const oldMessage = await this.getMessage(editedMessage.key, true);
|
||||
if ((oldMessage as any)?.id) {
|
||||
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
|
||||
? Math.floor(editedMessage.timestampMs.toNumber() / 1000)
|
||||
: Math.floor((editedMessage.timestampMs as number) / 1000);
|
||||
|
||||
await this.prismaRepository.message.update({
|
||||
where: { id: (oldMessage as any).id },
|
||||
data: {
|
||||
message: editedMessage.editedMessage as any,
|
||||
messageTimestamp: editedMessageTimestamp,
|
||||
status: 'EDITED',
|
||||
},
|
||||
});
|
||||
await this.prismaRepository.messageUpdate.create({
|
||||
data: {
|
||||
fromMe: editedMessage.key.fromMe,
|
||||
keyId: editedMessage.key.id,
|
||||
remoteJid: editedMessage.key.remoteJid,
|
||||
status: 'EDITED',
|
||||
instanceId: this.instanceId,
|
||||
messageId: (oldMessage as any).id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
|
||||
this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
|
||||
|
||||
await this.baileysCache.set(received.key.id, {
|
||||
message: received,
|
||||
retry: 0,
|
||||
});
|
||||
const messageKey = `${this.instance.id}_${received.key.id}`;
|
||||
const cached = await this.baileysCache.get(messageKey);
|
||||
|
||||
if (cached && !editedMessage) {
|
||||
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const retryCache = (await this.baileysCache.get(received.key.id)) || null;
|
||||
|
||||
if (retryCache) {
|
||||
this.logger.info('Recovered message lost');
|
||||
await this.baileysCache.delete(received.key.id);
|
||||
}
|
||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
||||
|
||||
if (
|
||||
(type !== 'notify' && type !== 'append') ||
|
||||
@ -1177,6 +1223,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingChat = await this.prismaRepository.chat.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
|
||||
select: { id: true, name: true },
|
||||
@ -1186,7 +1233,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
existingChat &&
|
||||
received.pushName &&
|
||||
existingChat.name !== received.pushName &&
|
||||
received.pushName.trim().length > 0
|
||||
received.pushName.trim().length > 0 &&
|
||||
!received.key.fromMe &&
|
||||
!received.key.remoteJid.includes('@g.us')
|
||||
) {
|
||||
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
|
||||
@ -1249,11 +1298,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
});
|
||||
|
||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
received,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(received, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1262,21 +1307,31 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
data: messageRaw,
|
||||
});
|
||||
|
||||
if (received.key.fromMe === false) {
|
||||
if (msg.status === status[3]) {
|
||||
this.logger.log(`Update not read messages ${received.key.remoteJid}`);
|
||||
const { remoteJid } = received.key;
|
||||
const timestamp = msg.messageTimestamp;
|
||||
const fromMe = received.key.fromMe.toString();
|
||||
const messageKey = `${remoteJid}_${timestamp}_${fromMe}`;
|
||||
|
||||
await this.updateChatUnreadMessages(received.key.remoteJid);
|
||||
} else if (msg.status === status[4]) {
|
||||
this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`);
|
||||
const cachedTimestamp = await this.baileysCache.get(messageKey);
|
||||
|
||||
await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp);
|
||||
if (!cachedTimestamp) {
|
||||
if (!received.key.fromMe) {
|
||||
if (msg.status === status[3]) {
|
||||
this.logger.log(`Update not read messages ${remoteJid}`);
|
||||
await this.updateChatUnreadMessages(remoteJid);
|
||||
} else if (msg.status === status[4]) {
|
||||
this.logger.log(`Update readed messages ${remoteJid} - ${timestamp}`);
|
||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||
}
|
||||
} else {
|
||||
// is send message by me
|
||||
this.logger.log(`Update readed messages ${remoteJid} - ${timestamp}`);
|
||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||
}
|
||||
} else {
|
||||
// is send message by me
|
||||
this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`);
|
||||
|
||||
await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp);
|
||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
||||
} else {
|
||||
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
||||
}
|
||||
|
||||
if (isMedia) {
|
||||
@ -1292,7 +1347,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
|
||||
const fullName = join(
|
||||
`${this.instance.id}`,
|
||||
received.key.remoteJid,
|
||||
mediaType,
|
||||
`${Date.now()}_${fileName}`,
|
||||
);
|
||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
@ -1335,7 +1395,24 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
},
|
||||
);
|
||||
|
||||
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
|
||||
if (buffer) {
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
} else {
|
||||
// retry to download media
|
||||
const buffer = await downloadMediaMessage(
|
||||
{ key: received.key, message: received?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: this.client.updateMediaMessage,
|
||||
},
|
||||
);
|
||||
|
||||
if (buffer) {
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error converting media to base64', error?.message]);
|
||||
}
|
||||
@ -1422,6 +1499,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||
|
||||
const cached = await this.baileysCache.get(updateKey);
|
||||
|
||||
if (cached) {
|
||||
this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.baileysCache.set(updateKey, true, 30 * 60);
|
||||
|
||||
if (status[update.status] === 'READ' && key.fromMe) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
@ -1432,7 +1520,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (key.remoteJid !== 'status@broadcast') {
|
||||
if (key.remoteJid !== 'status@broadcast' && key.id !== undefined) {
|
||||
let pollUpdates: any;
|
||||
|
||||
if (update.pollUpdates) {
|
||||
@ -1460,19 +1548,20 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const message: any = {
|
||||
messageId: findMessage.id,
|
||||
keyId: key.id,
|
||||
remoteJid: key?.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.remoteJid,
|
||||
status: status[update.status] ?? 'DELETED',
|
||||
pollUpdates,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (update.message === null && update.status === undefined) {
|
||||
this.sendDataWebhook(Events.MESSAGES_DELETE, key);
|
||||
|
||||
const message: any = {
|
||||
messageId: findMessage.id,
|
||||
keyId: key.id,
|
||||
remoteJid: key.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.remoteJid,
|
||||
status: 'DELETED',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
||||
await this.prismaRepository.messageUpdate.create({
|
||||
data: message,
|
||||
@ -1491,29 +1580,32 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (!key.fromMe && key.remoteJid) {
|
||||
readChatToUpdate[key.remoteJid] = true;
|
||||
|
||||
if (status[update.status] === status[4]) {
|
||||
this.logger.log(`Update as read ${key.remoteJid} - ${findMessage.messageTimestamp}`);
|
||||
this.updateMessagesReadedByTimestamp(key.remoteJid, findMessage.messageTimestamp);
|
||||
const { remoteJid } = key;
|
||||
const timestamp = findMessage.messageTimestamp;
|
||||
const fromMe = key.fromMe.toString();
|
||||
const messageKey = `${remoteJid}_${timestamp}_${fromMe}`;
|
||||
|
||||
const cachedTimestamp = await this.baileysCache.get(messageKey);
|
||||
|
||||
if (!cachedTimestamp) {
|
||||
if (status[update.status] === status[4]) {
|
||||
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
|
||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
||||
}
|
||||
|
||||
await this.prismaRepository.message.update({
|
||||
where: { id: findMessage.id },
|
||||
data: { status: status[update.status] },
|
||||
});
|
||||
} else {
|
||||
this.logger.info(
|
||||
`Update readed messages duplicated ignored in message.update [avoid deadlock]: ${messageKey}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.prismaRepository.message.update({
|
||||
where: { id: findMessage.id },
|
||||
data: { status: status[update.status] },
|
||||
});
|
||||
}
|
||||
|
||||
const message: any = {
|
||||
messageId: findMessage.id,
|
||||
keyId: key.id,
|
||||
remoteJid: key.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.remoteJid,
|
||||
status: status[update.status],
|
||||
pollUpdates,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
||||
|
||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
||||
@ -1529,7 +1621,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const chatToInsert = {
|
||||
remoteJid: message.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
name: message.pushName || '',
|
||||
unreadMessages: 0,
|
||||
};
|
||||
|
||||
@ -2233,11 +2324,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
});
|
||||
|
||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
messageRaw,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(messageRaw, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2309,7 +2396,24 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
},
|
||||
);
|
||||
|
||||
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
|
||||
if (buffer) {
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
} else {
|
||||
// retry to download media
|
||||
const buffer = await downloadMediaMessage(
|
||||
{ key: messageRaw.key, message: messageRaw?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: this.client.updateMediaMessage,
|
||||
},
|
||||
);
|
||||
|
||||
if (buffer) {
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error converting media to base64', error?.message]);
|
||||
}
|
||||
@ -2654,9 +2758,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
prepareMedia[mediaType].fileName = mediaMessage.fileName;
|
||||
|
||||
if (mediaMessage.mediatype === 'video') {
|
||||
prepareMedia[mediaType].jpegThumbnail = Uint8Array.from(
|
||||
readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')),
|
||||
);
|
||||
prepareMedia[mediaType].gifPlayback = false;
|
||||
}
|
||||
|
||||
@ -2703,21 +2804,43 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
imageBuffer = Buffer.from(response.data, 'binary');
|
||||
}
|
||||
|
||||
const webpBuffer = await sharp(imageBuffer).webp().toBuffer();
|
||||
const isAnimated = this.isAnimated(image, imageBuffer);
|
||||
|
||||
return webpBuffer;
|
||||
if (isAnimated) {
|
||||
return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer();
|
||||
} else {
|
||||
return await sharp(imageBuffer).webp().toBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao converter a imagem para WebP:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private isAnimatedWebp(buffer: Buffer): boolean {
|
||||
if (buffer.length < 12) return false;
|
||||
|
||||
return buffer.indexOf(Buffer.from('ANIM')) !== -1;
|
||||
}
|
||||
|
||||
private isAnimated(image: string, buffer: Buffer): boolean {
|
||||
const lowerCaseImage = image.toLowerCase();
|
||||
|
||||
if (lowerCaseImage.includes('.gif')) return true;
|
||||
|
||||
if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async mediaSticker(data: SendStickerDto, file?: any) {
|
||||
const mediaData: SendStickerDto = { ...data };
|
||||
|
||||
if (file) mediaData.sticker = file.buffer.toString('base64');
|
||||
|
||||
const convert = await this.convertToWebP(data.sticker);
|
||||
const convert = data?.notConvertSticker
|
||||
? Buffer.from(data.sticker, 'base64')
|
||||
: await this.convertToWebP(data.sticker);
|
||||
const gifPlayback = data.sticker.includes('.gif');
|
||||
const result = await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
@ -2923,7 +3046,29 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
.noVideo()
|
||||
.audioCodec('libopus')
|
||||
.addOutputOptions('-avoid_negative_ts make_zero')
|
||||
.audioBitrate('128k')
|
||||
.audioFrequency(48000)
|
||||
.audioChannels(1)
|
||||
.outputOptions([
|
||||
'-write_xing',
|
||||
'0',
|
||||
'-compression_level',
|
||||
'10',
|
||||
'-application',
|
||||
'voip',
|
||||
'-fflags',
|
||||
'+bitexact',
|
||||
'-flags',
|
||||
'+bitexact',
|
||||
'-id3v2_version',
|
||||
'0',
|
||||
'-map_metadata',
|
||||
'-1',
|
||||
'-map_chapters',
|
||||
'-1',
|
||||
'-write_bext',
|
||||
'0',
|
||||
])
|
||||
.pipe(outputAudioStream, { end: true })
|
||||
.on('error', function (error) {
|
||||
console.log('error', error);
|
||||
@ -3573,6 +3718,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
status: 'DELETED',
|
||||
},
|
||||
});
|
||||
const messageUpdate: any = {
|
||||
messageId: message.id,
|
||||
keyId: messageId,
|
||||
remoteJid: response.key.remoteJid,
|
||||
fromMe: response.key.fromMe,
|
||||
participant: response.key?.remoteJid,
|
||||
status: 'DELETED',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
await this.prismaRepository.messageUpdate.create({
|
||||
data: messageUpdate,
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.message.deleteMany({
|
||||
where: {
|
||||
@ -3618,6 +3775,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
|
||||
throw 'The message is messageContextInfo';
|
||||
}
|
||||
|
||||
let mediaMessage: any;
|
||||
let mediaType: string;
|
||||
|
||||
@ -3899,13 +4060,84 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.client.sendMessage(jid, {
|
||||
const oldMessage: any = await this.getMessage(data.key, true);
|
||||
if (!oldMessage) throw new NotFoundException('Message not found');
|
||||
if (oldMessage?.key?.remoteJid !== jid) {
|
||||
throw new BadRequestException('RemoteJid does not match');
|
||||
}
|
||||
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
|
||||
// 15 minutes in milliseconds
|
||||
throw new BadRequestException('Message is older than 15 minutes');
|
||||
}
|
||||
|
||||
const messageSent = await this.client.sendMessage(jid, {
|
||||
...(options as any),
|
||||
edit: data.key,
|
||||
});
|
||||
if (messageSent) {
|
||||
const editedMessage =
|
||||
messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
|
||||
|
||||
if (editedMessage) {
|
||||
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
|
||||
this.chatwootService.eventWhatsapp(
|
||||
'send.message.update',
|
||||
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
||||
editedMessage,
|
||||
);
|
||||
|
||||
const messageId = messageSent.message?.protocolMessage?.key?.id;
|
||||
if (messageId) {
|
||||
let message = await this.prismaRepository.message.findFirst({
|
||||
where: {
|
||||
key: {
|
||||
path: ['id'],
|
||||
equals: messageId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!message) throw new NotFoundException('Message not found');
|
||||
|
||||
if (!(message.key.valueOf() as any).fromMe) {
|
||||
new BadRequestException('You cannot edit others messages');
|
||||
}
|
||||
if ((message.key.valueOf() as any)?.deleted) {
|
||||
new BadRequestException('You cannot edit deleted messages');
|
||||
}
|
||||
if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') {
|
||||
oldMessage.message.conversation = data.text;
|
||||
} else {
|
||||
oldMessage.message[oldMessage.messageType].caption = data.text;
|
||||
}
|
||||
message = await this.prismaRepository.message.update({
|
||||
where: { id: message.id },
|
||||
data: {
|
||||
message: oldMessage.message,
|
||||
status: 'EDITED',
|
||||
messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds
|
||||
},
|
||||
});
|
||||
const messageUpdate: any = {
|
||||
messageId: message.id,
|
||||
keyId: messageId,
|
||||
remoteJid: messageSent.key.remoteJid,
|
||||
fromMe: messageSent.key.fromMe,
|
||||
participant: messageSent.key?.remoteJid,
|
||||
status: 'EDITED',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
await this.prismaRepository.messageUpdate.create({
|
||||
data: messageUpdate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messageSent;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new BadRequestException(error.toString());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4299,7 +4531,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
const messageRaw = {
|
||||
key: message.key,
|
||||
pushName: message.pushName,
|
||||
pushName:
|
||||
message.pushName ||
|
||||
(message.key.fromMe
|
||||
? 'Você'
|
||||
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
||||
status: status[message.status],
|
||||
message: { ...message.message },
|
||||
contextInfo: contentMsg?.contextInfo,
|
||||
@ -4534,4 +4770,245 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
//Business Controller
|
||||
public async fetchCatalog(instanceName: string, data: getCollectionsDto) {
|
||||
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
|
||||
const limit = data.limit || 10;
|
||||
const cursor = null;
|
||||
|
||||
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
||||
|
||||
if (!onWhatsapp.exists) {
|
||||
throw new BadRequestException(onWhatsapp);
|
||||
}
|
||||
|
||||
try {
|
||||
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
||||
const business = await this.fetchBusinessProfile(info?.jid);
|
||||
|
||||
let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor });
|
||||
let nextPageCursor = catalog.nextPageCursor;
|
||||
let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
|
||||
let pagination = nextPageCursorJson?.pagination_cursor
|
||||
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
|
||||
: null;
|
||||
let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
|
||||
|
||||
let productsCatalog = catalog.products || [];
|
||||
let countLoops = 0;
|
||||
while (fetcherHasMore && countLoops < 4) {
|
||||
catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor });
|
||||
nextPageCursor = catalog.nextPageCursor;
|
||||
nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
|
||||
pagination = nextPageCursorJson?.pagination_cursor
|
||||
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
|
||||
: null;
|
||||
fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
|
||||
productsCatalog = [...productsCatalog, ...catalog.products];
|
||||
countLoops++;
|
||||
}
|
||||
|
||||
return {
|
||||
wuid: info?.jid || jid,
|
||||
numberExists: info?.exists,
|
||||
isBusiness: business.isBusiness,
|
||||
catalogLength: productsCatalog.length,
|
||||
catalog: productsCatalog,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
wuid: jid,
|
||||
name: null,
|
||||
isBusiness: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async getCatalog({
|
||||
jid,
|
||||
limit,
|
||||
cursor,
|
||||
}: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> {
|
||||
try {
|
||||
jid = jid ? createJid(jid) : this.instance.wuid;
|
||||
|
||||
const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor });
|
||||
|
||||
if (!catalog) {
|
||||
return {
|
||||
products: undefined,
|
||||
nextPageCursor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return catalog;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException('Error getCatalog', error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchCollections(instanceName: string, data: getCollectionsDto) {
|
||||
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
|
||||
const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque)
|
||||
|
||||
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
||||
|
||||
if (!onWhatsapp.exists) {
|
||||
throw new BadRequestException(onWhatsapp);
|
||||
}
|
||||
|
||||
try {
|
||||
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
||||
const business = await this.fetchBusinessProfile(info?.jid);
|
||||
const collections = await this.getCollections(info?.jid, limit);
|
||||
|
||||
return {
|
||||
wuid: info?.jid || jid,
|
||||
name: info?.name,
|
||||
numberExists: info?.exists,
|
||||
isBusiness: business.isBusiness,
|
||||
collectionsLength: collections?.length,
|
||||
collections: collections,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
wuid: jid,
|
||||
name: null,
|
||||
isBusiness: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async getCollections(jid?: string | undefined, limit?: number): Promise<CatalogCollection[]> {
|
||||
try {
|
||||
jid = jid ? createJid(jid) : this.instance.wuid;
|
||||
|
||||
const result = await this.client.getCollections(jid, limit);
|
||||
|
||||
if (!result) {
|
||||
return [
|
||||
{
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
products: [],
|
||||
status: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return result.collections;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException('Error getCatalog', error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchMessages(query: Query<Message>) {
|
||||
const keyFilters = query?.where?.key as {
|
||||
id?: string;
|
||||
fromMe?: boolean;
|
||||
remoteJid?: string;
|
||||
participants?: string;
|
||||
};
|
||||
|
||||
const timestampFilter = {};
|
||||
if (query?.where?.messageTimestamp) {
|
||||
if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) {
|
||||
timestampFilter['messageTimestamp'] = {
|
||||
gte: Math.floor(new Date(query.where.messageTimestamp['gte']).getTime() / 1000),
|
||||
lte: Math.floor(new Date(query.where.messageTimestamp['lte']).getTime() / 1000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const count = await this.prismaRepository.message.count({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
id: query?.where?.id,
|
||||
source: query?.where?.source,
|
||||
messageType: query?.where?.messageType,
|
||||
...timestampFilter,
|
||||
AND: [
|
||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||
keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!query?.offset) {
|
||||
query.offset = 50;
|
||||
}
|
||||
|
||||
if (!query?.page) {
|
||||
query.page = 1;
|
||||
}
|
||||
|
||||
const messages = await this.prismaRepository.message.findMany({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
id: query?.where?.id,
|
||||
source: query?.where?.source,
|
||||
messageType: query?.where?.messageType,
|
||||
...timestampFilter,
|
||||
AND: [
|
||||
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
||||
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
||||
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
||||
keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
messageTimestamp: 'desc',
|
||||
},
|
||||
skip: query.offset * (query?.page === 1 ? 0 : (query?.page as number) - 1),
|
||||
take: query.offset,
|
||||
select: {
|
||||
id: true,
|
||||
key: true,
|
||||
pushName: true,
|
||||
messageType: true,
|
||||
message: true,
|
||||
messageTimestamp: true,
|
||||
instanceId: true,
|
||||
source: true,
|
||||
contextInfo: true,
|
||||
MessageUpdate: {
|
||||
select: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const formattedMessages = messages.map((message) => {
|
||||
const messageKey = message.key as { fromMe: boolean; remoteJid: string; id: string; participant?: string };
|
||||
|
||||
if (!message.pushName) {
|
||||
if (messageKey.fromMe) {
|
||||
message.pushName = 'Você';
|
||||
} else if (message.contextInfo) {
|
||||
const contextInfo = message.contextInfo as { participant?: string };
|
||||
if (contextInfo.participant) {
|
||||
message.pushName = contextInfo.participant.split('@')[0];
|
||||
} else if (messageKey.participant) {
|
||||
message.pushName = messageKey.participant.split('@')[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
return {
|
||||
messages: {
|
||||
total: count,
|
||||
pages: Math.ceil(count / query.offset),
|
||||
currentPage: query.page,
|
||||
records: formattedMessages,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
929
src/api/integrations/chatbot/base-chatbot.controller.ts
Normal file
929
src/api/integrations/chatbot/base-chatbot.controller.ts
Normal file
@ -0,0 +1,929 @@
|
||||
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) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
42
src/api/integrations/chatbot/base-chatbot.dto.ts
Normal file
42
src/api/integrations/chatbot/base-chatbot.dto.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Base DTO for all chatbot integrations
|
||||
* Contains common properties shared by all chatbot types
|
||||
*/
|
||||
export class BaseChatbotDto {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base settings DTO for all chatbot integrations
|
||||
*/
|
||||
export class BaseChatbotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
fallbackId?: string; // Unified fallback ID field for all integrations
|
||||
}
|
412
src/api/integrations/chatbot/base-chatbot.service.ts
Normal file
412
src/api/integrations/chatbot/base-chatbot.service.ts
Normal file
@ -0,0 +1,412 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { IntegrationSession } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Base class for all chatbot service implementations
|
||||
* Contains common methods shared across different chatbot integrations
|
||||
*/
|
||||
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
protected readonly logger: Logger;
|
||||
protected readonly waMonitor: WAMonitoringService;
|
||||
protected readonly prismaRepository: PrismaRepository;
|
||||
protected readonly configService?: ConfigService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
loggerName: string,
|
||||
configService?: ConfigService,
|
||||
) {
|
||||
this.waMonitor = waMonitor;
|
||||
this.prismaRepository = prismaRepository;
|
||||
this.logger = new Logger(loggerName);
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains an image
|
||||
*/
|
||||
protected isImageMessage(content: string): boolean {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains audio
|
||||
*/
|
||||
protected isAudioMessage(content: string): boolean {
|
||||
return content.includes('audioMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is valid JSON
|
||||
*/
|
||||
protected isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the media type from a URL based on its extension
|
||||
*/
|
||||
protected getMediaType(url: string): string | null {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chatbot session
|
||||
*/
|
||||
public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
|
||||
try {
|
||||
// Extract pushName safely - if data.pushName is an object with a pushName property, use that
|
||||
const pushNameValue =
|
||||
typeof data.pushName === 'object' && data.pushName?.pushName
|
||||
? data.pushName.pushName
|
||||
: typeof data.pushName === 'string'
|
||||
? data.pushName
|
||||
: null;
|
||||
|
||||
// Extract remoteJid safely
|
||||
const remoteJidValue =
|
||||
typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;
|
||||
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: remoteJidValue,
|
||||
pushName: pushNameValue,
|
||||
sessionId: remoteJidValue,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for processing incoming messages
|
||||
* This handles the common workflow across all chatbot types:
|
||||
* 1. Check for existing session or create new one
|
||||
* 2. Handle message based on session state
|
||||
*/
|
||||
public async process(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
session: IntegrationSession,
|
||||
settings: SettingsType,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// For new sessions or sessions awaiting initialization
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// If session is paused, ignore the message
|
||||
if (session.status === 'paused') {
|
||||
return;
|
||||
}
|
||||
|
||||
// For existing sessions, keywords might indicate the conversation should end
|
||||
const keywordFinish = (settings as any)?.keywordFinish || '';
|
||||
const normalizedContent = content.toLowerCase().trim();
|
||||
if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
|
||||
// Update session to closed and return
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward the message to the chatbot API
|
||||
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
|
||||
|
||||
// Update session to indicate we're waiting for user response
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in process: ${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for sending messages to WhatsApp
|
||||
* This handles common patterns like markdown links and formatting
|
||||
*/
|
||||
protected async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
message: string,
|
||||
settings: SettingsType,
|
||||
): Promise<void> {
|
||||
if (!message) return;
|
||||
|
||||
const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const splitMessages = (settings as any)?.splitMessages ?? false;
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, altText, url] = match;
|
||||
const mediaType = this.getMediaType(url);
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
// Send accumulated text before sending media
|
||||
if (textBuffer.trim()) {
|
||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
// Handle sending the media
|
||||
try {
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
fileName: mediaType === 'document' ? altText || 'document' : undefined,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error sending media: ${error}`);
|
||||
// If media fails, at least send the alt text and URL
|
||||
textBuffer += `${altText}: ${url}`;
|
||||
}
|
||||
} else {
|
||||
// It's a regular link, keep it in the text
|
||||
textBuffer += fullMatch;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
// Send any remaining text
|
||||
if (textBuffer.trim()) {
|
||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to send formatted text with proper typing indicators and delays
|
||||
*/
|
||||
private async sendFormattedText(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
text: string,
|
||||
settings: any,
|
||||
splitMessages: boolean,
|
||||
): Promise<void> {
|
||||
const timePerChar = settings?.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (splitMessages) {
|
||||
const multipleMessages = text.split('\n\n');
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
if (!message.trim()) continue;
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: text,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for initializing a new session
|
||||
* This method should be overridden if a subclass needs specific initialization
|
||||
*/
|
||||
protected async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
settings: SettingsType,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string | any,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
// Create a session if none exists
|
||||
if (!session) {
|
||||
// Extract pushName properly - if it's an object with pushName property, use that
|
||||
const pushNameValue =
|
||||
typeof pushName === 'object' && pushName?.pushName
|
||||
? pushName.pushName
|
||||
: typeof pushName === 'string'
|
||||
? pushName
|
||||
: null;
|
||||
|
||||
const sessionResult = await this.createNewSession(
|
||||
{
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
{
|
||||
remoteJid,
|
||||
pushName: pushNameValue,
|
||||
botId: (bot as any).id,
|
||||
},
|
||||
this.getBotType(),
|
||||
);
|
||||
|
||||
if (!sessionResult || !sessionResult.session) {
|
||||
this.logger.error('Failed to create new session');
|
||||
return;
|
||||
}
|
||||
|
||||
session = sessionResult.session;
|
||||
}
|
||||
|
||||
// Update session status to opened
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Forward the message to the chatbot
|
||||
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
|
||||
* This should match the type field used in the IntegrationSession
|
||||
*/
|
||||
protected abstract getBotType(): string;
|
||||
|
||||
/**
|
||||
* Send a message to the chatbot API
|
||||
* This is specific to each chatbot integration
|
||||
*/
|
||||
protected abstract sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: SettingsType,
|
||||
bot: BotType,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void>;
|
||||
}
|
@ -2,8 +2,10 @@ import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import {
|
||||
difyController,
|
||||
evoaiController,
|
||||
evolutionBotController,
|
||||
flowiseController,
|
||||
n8nController,
|
||||
openaiController,
|
||||
typebotController,
|
||||
} from '@api/server.module';
|
||||
@ -97,6 +99,10 @@ export class ChatbotController {
|
||||
|
||||
await difyController.emit(emitData);
|
||||
|
||||
await n8nController.emit(emitData);
|
||||
|
||||
await evoaiController.emit(emitData);
|
||||
|
||||
await flowiseController.emit(emitData);
|
||||
}
|
||||
|
||||
@ -173,7 +179,7 @@ export class ChatbotController {
|
||||
if (session) {
|
||||
if (session.status !== 'closed' && !session.botId) {
|
||||
this.logger.warn('Session is already opened in another integration');
|
||||
return;
|
||||
return null;
|
||||
} else if (!session.botId) {
|
||||
session = null;
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou
|
||||
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
import { EvoaiRouter } from './evoai/routes/evoai.router';
|
||||
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
|
||||
import { FlowiseRouter } from './flowise/routes/flowise.router';
|
||||
import { N8nRouter } from './n8n/routes/n8n.router';
|
||||
|
||||
export class ChatbotRouter {
|
||||
public readonly router: Router;
|
||||
@ -19,5 +21,7 @@ export class ChatbotRouter {
|
||||
this.router.use('/openai', new OpenaiRouter(...guards).router);
|
||||
this.router.use('/dify', new DifyRouter(...guards).router);
|
||||
this.router.use('/flowise', new FlowiseRouter(...guards).router);
|
||||
this.router.use('/n8n', new N8nRouter(...guards).router);
|
||||
this.router.use('/evoai', new EvoaiRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
|
||||
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
|
||||
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
|
||||
export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema';
|
||||
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
|
||||
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
|
||||
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
|
||||
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';
|
||||
|
@ -543,216 +543,220 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
const remoteJid = body.key.remoteJid;
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||
const maxWaitTime = 5000; // 5 secounds
|
||||
|
||||
try {
|
||||
this.logger.verbose('--- Start createConversation ---');
|
||||
this.logger.verbose(`--- Start createConversation ---`);
|
||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||||
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
|
||||
this.logger.verbose(`Cache key: ${cacheKey}`);
|
||||
|
||||
// If it already exists in the cache, return conversationId
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Cached conversation ID: ${conversationId}`);
|
||||
let conversationExists: conversation | boolean;
|
||||
try {
|
||||
conversationExists = await client.conversations.get({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversationId,
|
||||
});
|
||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting conversation: ${error}`);
|
||||
conversationExists = false;
|
||||
}
|
||||
if (!conversationExists) {
|
||||
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
||||
this.cache.delete(cacheKey);
|
||||
return await this.createConversation(instance, body);
|
||||
}
|
||||
|
||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
const isGroup = body.key.remoteJid.includes('@g.us');
|
||||
this.logger.verbose(`Is group: ${isGroup}`);
|
||||
|
||||
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
|
||||
this.logger.verbose(`Chat ID: ${chatId}`);
|
||||
|
||||
let nameContact: string;
|
||||
|
||||
nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
this.logger.verbose(`Name contact: ${nameContact}`);
|
||||
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
|
||||
if (!filterInbox) {
|
||||
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
|
||||
return null;
|
||||
// If lock already exists, wait until release or timeout
|
||||
if (await this.cache.has(lockKey)) {
|
||||
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
|
||||
const start = Date.now();
|
||||
while (await this.cache.has(lockKey)) {
|
||||
if (Date.now() - start > maxWaitTime) {
|
||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||
break;
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
return conversationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
this.logger.verbose('Processing group conversation');
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
// Adquire lock
|
||||
await this.cache.set(lockKey, true, 30);
|
||||
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
|
||||
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
try {
|
||||
/*
|
||||
Double check after lock
|
||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||||
*/
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
const client = await this.clientCw(instance);
|
||||
if (!client) return null;
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
const isGroup = remoteJid.includes('@g.us');
|
||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
if (!filterInbox) return null;
|
||||
|
||||
if (findParticipant) {
|
||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||
await this.updateContact(instance, findParticipant.id, {
|
||||
name: body.pushName,
|
||||
avatar_url: picture_url.profilePictureUrl || null,
|
||||
});
|
||||
if (isGroup) {
|
||||
this.logger.verbose(`Processing group conversation`);
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
||||
await this.updateContact(instance, findParticipant.id, {
|
||||
name: body.pushName,
|
||||
avatar_url: picture_url.profilePictureUrl || null,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
|
||||
if (contact) {
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
if (!body.key.fromMe) {
|
||||
const waProfilePictureFile =
|
||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
||||
const nameNeedsUpdate =
|
||||
!contact.name ||
|
||||
contact.name === chatId ||
|
||||
(`+${chatId}`.startsWith('+55')
|
||||
? this.getNumbers(`+${chatId}`).some(
|
||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
||||
)
|
||||
: false);
|
||||
|
||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||
|
||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||
contact = await this.updateContact(instance, contact.id, {
|
||||
...(nameNeedsUpdate && { name: nameContact }),
|
||||
...(waProfilePictureFile === '' && { avatar: null }),
|
||||
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.createContact(
|
||||
const jid = body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
chatId,
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
jid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
|
||||
if (contact) {
|
||||
if (!body.key.fromMe) {
|
||||
const waProfilePictureFile =
|
||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
||||
const nameNeedsUpdate =
|
||||
!contact.name ||
|
||||
contact.name === chatId ||
|
||||
(`+${chatId}`.startsWith('+55')
|
||||
? this.getNumbers(`+${chatId}`).some(
|
||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
||||
)
|
||||
: false);
|
||||
|
||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||
|
||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||
contact = await this.updateContact(instance, contact.id, {
|
||||
...(nameNeedsUpdate && { name: nameContact }),
|
||||
...(waProfilePictureFile === '' && { avatar: null }),
|
||||
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
|
||||
});
|
||||
}
|
||||
if (!contact) {
|
||||
this.logger.warn(`Contact not created or found`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const jid = body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
chatId,
|
||||
filterInbox.id,
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
jid,
|
||||
|
||||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
||||
this.logger.verbose(`Contact ID: ${contactId}`);
|
||||
|
||||
const contactConversations = (await client.contacts.listConversations({
|
||||
accountId: this.provider.accountId,
|
||||
id: contactId,
|
||||
})) as any;
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error(`No conversations found or payload is undefined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let inboxConversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
}
|
||||
if (inboxConversation) {
|
||||
if (this.provider.reopenConversation) {
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
||||
|
||||
if (!contact) {
|
||||
this.logger.warn('Contact not created or found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
||||
this.logger.verbose(`Contact ID: ${contactId}`);
|
||||
|
||||
const contactConversations = (await client.contacts.listConversations({
|
||||
accountId: this.provider.accountId,
|
||||
id: contactId,
|
||||
})) as any;
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error('No conversations found or payload is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contactConversations.payload.length) {
|
||||
let conversation: any;
|
||||
if (this.provider.reopenConversation) {
|
||||
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
|
||||
|
||||
if (this.provider.conversationPending && conversation.status !== 'open') {
|
||||
if (conversation) {
|
||||
if (this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||||
await client.conversations.toggleStatus({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversation.id,
|
||||
conversationId: inboxConversation.id,
|
||||
data: {
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
inboxConversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
||||
}
|
||||
|
||||
if (inboxConversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||
this.cache.set(cacheKey, inboxConversation.id);
|
||||
return inboxConversation.id;
|
||||
}
|
||||
} else {
|
||||
conversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
|
||||
}
|
||||
|
||||
if (conversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
const data = {
|
||||
contact_id: contactId.toString(),
|
||||
inbox_id: filterInbox.id.toString(),
|
||||
};
|
||||
|
||||
if (this.provider.conversationPending) {
|
||||
data['status'] = 'pending';
|
||||
}
|
||||
|
||||
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;
|
||||
} finally {
|
||||
await this.cache.delete(lockKey);
|
||||
this.logger.verbose(`Block released for: ${lockKey}`);
|
||||
}
|
||||
|
||||
const data = {
|
||||
contact_id: contactId.toString(),
|
||||
inbox_id: filterInbox.id.toString(),
|
||||
};
|
||||
|
||||
if (this.provider.conversationPending) {
|
||||
data['status'] = 'pending';
|
||||
}
|
||||
|
||||
const conversation = await client.conversations.create({
|
||||
accountId: this.provider.accountId,
|
||||
data,
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.warn('Conversation not created or found');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in createConversation: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -931,7 +935,7 @@ export class ChatwootService {
|
||||
quotedMsg?: MessageModel,
|
||||
) {
|
||||
if (sourceId && this.isImportHistoryAvailable()) {
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
||||
if (messageAlreadySaved) {
|
||||
if (messageAlreadySaved.size > 0) {
|
||||
this.logger.warn('Message already saved on chatwoot');
|
||||
@ -1106,12 +1110,13 @@ export class ChatwootService {
|
||||
|
||||
sendTelemetry('/message/sendWhatsAppAudio');
|
||||
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, true);
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
|
||||
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||
type = 'document';
|
||||
}
|
||||
|
||||
@ -1653,7 +1658,7 @@ export class ChatwootService {
|
||||
stickerMessage: undefined,
|
||||
documentMessage: msg.documentMessage?.caption,
|
||||
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
|
||||
audioMessage: msg.audioMessage?.caption,
|
||||
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
|
||||
contactMessage: msg.contactMessage?.vcard,
|
||||
contactsArrayMessage: msg.contactsArrayMessage,
|
||||
locationMessage: msg.locationMessage,
|
||||
@ -1898,7 +1903,7 @@ export class ChatwootService {
|
||||
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
|
||||
: originalMessage;
|
||||
|
||||
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
|
||||
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2199,7 +2204,7 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
if (event === 'messages.edit') {
|
||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||
const editedText = `${
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||||
|
@ -169,7 +169,7 @@ class ChatwootImport {
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
|
||||
public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise<Set<string>> {
|
||||
try {
|
||||
const existingSourceIdsSet = new Set<string>();
|
||||
|
||||
@ -177,18 +177,25 @@ class ChatwootImport {
|
||||
return existingSourceIdsSet;
|
||||
}
|
||||
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
|
||||
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
// Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
const result = await pgClient.query(query, [formattedSourceIds]);
|
||||
|
||||
const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds];
|
||||
|
||||
const query = conversationId
|
||||
? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'
|
||||
: 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
|
||||
const result = await pgClient.query(query, params);
|
||||
for (const row of result.rows) {
|
||||
existingSourceIdsSet.add(row.source_id);
|
||||
}
|
||||
|
||||
return existingSourceIdsSet;
|
||||
} catch (error) {
|
||||
return null;
|
||||
this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`);
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,25 +506,30 @@ class ChatwootImport {
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
|
||||
switch (typeKey) {
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
case 'documentMessage': {
|
||||
const doc = msg.message.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
case 'documentWithCaptionMessage': {
|
||||
const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
|
||||
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
|
||||
case 'templateMessage': {
|
||||
const template = msg.message.templateMessage?.hydratedTemplate;
|
||||
return (
|
||||
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
|
||||
(template?.hydratedContentText || '')
|
||||
);
|
||||
}
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
@ -7,12 +6,11 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Dify } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Dify as DifyModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class DifyController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
constructor(
|
||||
private readonly difyService: DifyService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -26,6 +24,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('DifyController');
|
||||
protected readonly integrationName = 'Dify';
|
||||
|
||||
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
|
||||
botRepository: any;
|
||||
@ -33,261 +32,37 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating dify');
|
||||
}
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'difyIdFallback';
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
protected getIntegrationType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
protected getAdditionalBotData(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
@ -303,81 +78,10 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating dify');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
// Override createBot to add Dify-specific validation
|
||||
public async createBot(instance: InstanceDto, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
@ -388,501 +92,35 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
// Dify-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting dify bot');
|
||||
}
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
difyIdFallback: updateSettings.difyIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
difyIdFallback: newSetttings.difyIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
difyIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
difyIdFallback: settings.difyIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.difyIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.difyIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Process Dify-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: DifyModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,13 @@
|
||||
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { $Enums } from '@prisma/client';
|
||||
|
||||
export class DifyDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class DifyDto extends BaseChatbotDto {
|
||||
botType?: $Enums.DifyBotType;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class DifySettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class DifySettingDto extends BaseChatbotSettingDto {
|
||||
difyIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,60 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export class DifyService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('DifyService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'DifyService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
/**
|
||||
* Return the bot type for Dify
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
private isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageToBot(
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
@ -62,10 +36,30 @@ export class DifyService {
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
) {
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
let endpoint: string = dify.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Dify endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
let processedContent = content;
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dify.botType === 'chatBot') {
|
||||
endpoint += '/chat-messages';
|
||||
const payload: any = {
|
||||
@ -74,17 +68,17 @@ export class DifyService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: content,
|
||||
query: processedContent,
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -112,7 +106,9 @@ export class DifyService {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -130,21 +126,21 @@ export class DifyService {
|
||||
endpoint += '/completion-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: content,
|
||||
query: processedContent,
|
||||
pushName: pushName,
|
||||
remoteJid: remoteJid,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -172,7 +168,9 @@ export class DifyService {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -194,17 +192,17 @@ export class DifyService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: content,
|
||||
query: processedContent,
|
||||
response_mode: 'streaming',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@ -230,7 +228,6 @@ export class DifyService {
|
||||
let answer = '';
|
||||
|
||||
const data = response.data.replaceAll('data: ', '');
|
||||
|
||||
const events = data.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
for (const eventString of events) {
|
||||
@ -248,9 +245,9 @@ export class DifyService {
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = answer;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
if (answer) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@ -259,369 +256,13 @@ export class DifyService {
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: conversationId,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dify.botType === 'workflow') {
|
||||
endpoint += '/workflows/run';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: content,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
},
|
||||
];
|
||||
payload.inputs.query = contentSplit[2] || content;
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.data.outputs.text;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.response?.data || error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
settings: DifySetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: dify.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processDify(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,122 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto';
|
||||
import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Evoai } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto> {
|
||||
constructor(
|
||||
private readonly evoaiService: EvoaiService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.evoai;
|
||||
this.settingsRepository = this.prismaRepository.evoaiSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvoaiController');
|
||||
protected readonly integrationName = 'Evoai';
|
||||
|
||||
integrationEnabled = configService.get<Evoai>('EVOAI').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.evoaiIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'evoaiIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: EvoaiDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add EvoAI-specific validation
|
||||
public async createBot(instance: InstanceDto, data: EvoaiDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// EvoAI-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process Evoai-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvoaiModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
10
src/api/integrations/chatbot/evoai/dto/evoai.dto.ts
Normal file
10
src/api/integrations/chatbot/evoai/dto/evoai.dto.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvoaiDto extends BaseChatbotDto {
|
||||
agentUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class EvoaiSettingDto extends BaseChatbotSettingDto {
|
||||
evoaiIdFallback?: string;
|
||||
}
|
124
src/api/integrations/chatbot/evoai/routes/evoai.router.ts
Normal file
124
src/api/integrations/chatbot/evoai/routes/evoai.router.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { evoaiController } from '@api/server.module';
|
||||
import {
|
||||
evoaiIgnoreJidSchema,
|
||||
evoaiSchema,
|
||||
evoaiSettingSchema,
|
||||
evoaiStatusSchema,
|
||||
instanceSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { EvoaiDto, EvoaiSettingDto } from '../dto/evoai.dto';
|
||||
|
||||
export class EvoaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.createBot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.updateBot(instance, req.params.evoaiId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.deleteBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiSettingDto>({
|
||||
request: req,
|
||||
schema: evoaiSettingSchema,
|
||||
ClassRef: EvoaiSettingDto,
|
||||
execute: (instance, data) => evoaiController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: evoaiStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => evoaiController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSessions(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: evoaiIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => evoaiController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
186
src/api/integrations/chatbot/evoai/services/evoai.service.ts
Normal file
186
src/api/integrations/chatbot/evoai/services/evoai.service.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { downloadMediaMessage } from 'baileys';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvoaiService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for EvoAI
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the abstract method to send message to EvoAI API
|
||||
* Handles audio transcription, image processing, and complex JSON-RPC payload
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvoaiSetting,
|
||||
evoai: Evoai,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
|
||||
|
||||
let processedContent = content;
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = 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;
|
||||
}
|
||||
}
|
||||
}
|
115
src/api/integrations/chatbot/evoai/validate/evoai.schema.ts
Normal file
115
src/api/integrations/chatbot/evoai/validate/evoai.schema.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const evoaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
agentUrl: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'agentUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'agentUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const evoaiStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const evoaiSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const evoaiIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
@ -1,16 +1,13 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { EvolutionBot } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { EvolutionBot, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { EvolutionBotDto } from '../dto/evolutionBot.dto';
|
||||
import { EvolutionBotService } from '../services/evolutionBot.service';
|
||||
|
||||
export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class EvolutionBotController extends BaseChatbotController<EvolutionBot, EvolutionBotDto> {
|
||||
constructor(
|
||||
private readonly evolutionBotService: EvolutionBotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -24,258 +21,49 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvolutionBotController');
|
||||
protected readonly integrationName = 'EvolutionBot';
|
||||
|
||||
integrationEnabled: boolean;
|
||||
integrationEnabled = true; // Set to true by default or use config value if available
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
// Implementation of abstract methods required by BaseChatbotController
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.botIdFallback;
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'botIdFallback';
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
protected getIntegrationType(): string {
|
||||
return 'evolution';
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
protected getAdditionalBotData(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(
|
||||
botId: string,
|
||||
instanceId: string,
|
||||
data: EvolutionBotDto,
|
||||
): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
@ -288,573 +76,21 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Bot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating bot');
|
||||
throw new Error('Evolution Bot already exists');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
botIdFallback: updateSettings.botIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
botIdFallback: newSetttings.botIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
botIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
botIdFallback: settings.botIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvolutionBot;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.botIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.botIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Process bot-specific logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,10 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvolutionBotDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
export class EvolutionBotDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class EvolutionBotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class EvolutionBotSettingDto extends BaseChatbotSettingDto {
|
||||
botIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,428 +1,138 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
|
||||
export class EvolutionBotService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvolutionBotService extends BaseChatbotService<EvolutionBot, EvolutionBotSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('EvolutionBotService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
/**
|
||||
* Get the bot type identifier
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evolution';
|
||||
}
|
||||
|
||||
private async sendMessageToBot(
|
||||
/**
|
||||
* Send a message to the Evolution Bot API
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
bot: EvolutionBot,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
) {
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
sessionId: session.id,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
query: content,
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
sessionId: session.id,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
];
|
||||
payload.query = contentSplit[2] || content;
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
let headers: any = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (bot.apiKey) {
|
||||
headers = {
|
||||
...headers,
|
||||
Authorization: `Bearer ${bot.apiKey}`,
|
||||
query: content,
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post(bot.apiUrl, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.message;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.query = transcription;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
payload.files = [
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
type: 'image',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
},
|
||||
false,
|
||||
);
|
||||
];
|
||||
payload.query = contentSplit[2] || content;
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
const endpoint = bot.apiUrl;
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
settings: EvolutionBotSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Evolution Bot endpoint defined');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
let headers: any = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
if (bot.apiKey) {
|
||||
headers = {
|
||||
...headers,
|
||||
Authorization: `Bearer ${bot.apiKey}`,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
let message = response?.data?.message;
|
||||
|
||||
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
|
||||
const innerContent = message.slice(1, -1);
|
||||
if (!innerContent.includes("'")) {
|
||||
message = innerContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
// Send telemetry
|
||||
sendTelemetry('/message/sendText');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Flowise } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Flowise } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { FlowiseDto } from '../dto/flowise.dto';
|
||||
import { FlowiseService } from '../services/flowise.service';
|
||||
|
||||
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
|
||||
constructor(
|
||||
private readonly flowiseService: FlowiseService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -24,15 +24,73 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('FlowiseController');
|
||||
protected readonly integrationName = 'Flowise';
|
||||
|
||||
integrationEnabled: boolean;
|
||||
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.flowiseIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'flowiseIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Flowise-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Override createBot to add module availability check and Flowise-specific validation
|
||||
public async createBot(instance: InstanceDto, data: FlowiseDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
@ -41,74 +99,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a Flowise with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
// Flowise-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
@ -121,740 +112,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: FlowiseDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Bot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
flowiseIdFallback: updateSettings.flowiseIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
flowiseIdFallback: newSetttings.flowiseIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
flowiseIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
flowiseIdFallback: settings.flowiseIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as Flowise;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.flowiseIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.flowiseIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,10 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class FlowiseDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
export class FlowiseDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class FlowiseSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
||||
flowiseIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -1,50 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
export class FlowiseService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('FlowiseService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
// Return the bot type for Flowise
|
||||
protected getBotType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
private async sendMessageToBot(instance: any, bot: Flowise, remoteJid: string, pushName: string, content: string) {
|
||||
// Process Flowise-specific bot logic
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Implement the abstract method to send message to Flowise API
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
bot: FlowiseModel,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
question: content,
|
||||
overrideConfig: {
|
||||
@ -54,11 +61,24 @@ export class FlowiseService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.question = transcription;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
@ -91,335 +111,26 @@ export class FlowiseService {
|
||||
|
||||
const endpoint = bot.apiUrl;
|
||||
|
||||
if (!endpoint) return null;
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Flowise endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
const message = response?.data?.text;
|
||||
|
||||
return message;
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
settings: FlowiseSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
// The service is now complete with just the abstract method implementations
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||
@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
flowiseIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
|
127
src/api/integrations/chatbot/n8n/controllers/n8n.controller.ts
Normal file
127
src/api/integrations/chatbot/n8n/controllers/n8n.controller.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { N8nDto } from '@api/integrations/chatbot/n8n/dto/n8n.dto';
|
||||
import { N8nService } from '@api/integrations/chatbot/n8n/services/n8n.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { IntegrationSession, N8n as N8nModel } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
|
||||
constructor(
|
||||
private readonly n8nService: N8nService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.n8n;
|
||||
this.settingsRepository = this.prismaRepository.n8nSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('N8nController');
|
||||
protected readonly integrationName = 'N8n';
|
||||
|
||||
integrationEnabled = configService.get('N8N').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'n8nIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'n8n';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: N8nDto): Record<string, any> {
|
||||
return {
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: N8nDto): Record<string, any> {
|
||||
return {
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: N8nDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('N8n already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: N8nDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Check for N8n-specific duplicate
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('N8n already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest of the bot creation process
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process N8n-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: N8nModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Use the base class pattern instead of calling n8nService.process directly
|
||||
await this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
17
src/api/integrations/chatbot/n8n/dto/n8n.dto.ts
Normal file
17
src/api/integrations/chatbot/n8n/dto/n8n.dto.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class N8nDto extends BaseChatbotDto {
|
||||
// N8n specific fields
|
||||
webhookUrl?: string;
|
||||
basicAuthUser?: string;
|
||||
basicAuthPass?: string;
|
||||
}
|
||||
|
||||
export class N8nSettingDto extends BaseChatbotSettingDto {
|
||||
// N8n has no specific fields
|
||||
}
|
||||
|
||||
export class N8nMessageDto {
|
||||
chatInput: string;
|
||||
sessionId: string;
|
||||
}
|
114
src/api/integrations/chatbot/n8n/routes/n8n.router.ts
Normal file
114
src/api/integrations/chatbot/n8n/routes/n8n.router.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { n8nController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
n8nIgnoreJidSchema,
|
||||
n8nSchema,
|
||||
n8nSettingSchema,
|
||||
n8nStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { N8nDto, N8nSettingDto } from '../dto/n8n.dto';
|
||||
|
||||
export class N8nRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nDto>({
|
||||
request: req,
|
||||
schema: n8nSchema,
|
||||
ClassRef: N8nDto,
|
||||
execute: (instance, data) => n8nController.createBot(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.findBot(instance),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchBot(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nDto>({
|
||||
request: req,
|
||||
schema: n8nSchema,
|
||||
ClassRef: N8nDto,
|
||||
execute: (instance, data) => n8nController.updateBot(instance, req.params.n8nId, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.deleteBot(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nSettingDto>({
|
||||
request: req,
|
||||
schema: n8nSettingSchema,
|
||||
ClassRef: N8nSettingDto,
|
||||
execute: (instance, data) => n8nController.settings(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchSettings(instance),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: n8nStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => n8nController.changeStatus(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchSessions(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: n8nIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => n8nController.ignoreJid(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
96
src/api/integrations/chatbot/n8n/services/n8n.service.ts
Normal file
96
src/api/integrations/chatbot/n8n/services/n8n.service.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'N8nService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for N8n
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'n8n';
|
||||
}
|
||||
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: N8nSetting,
|
||||
n8n: N8n,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
) {
|
||||
try {
|
||||
if (!session) {
|
||||
this.logger.error('Session is null in sendMessageToBot');
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint: string = n8n.webhookUrl;
|
||||
const payload: any = {
|
||||
chatInput: content,
|
||||
sessionId: session.sessionId,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.chatInput = 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;
|
||||
}
|
||||
}
|
||||
}
|
116
src/api/integrations/chatbot/n8n/validate/n8n.schema.ts
Normal file
116
src/api/integrations/chatbot/n8n/validate/n8n.schema.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const n8nSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
webhookUrl: { type: 'string' },
|
||||
basicAuthUser: { type: 'string' },
|
||||
basicAuthPassword: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'webhookUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'webhookUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const n8nStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const n8nSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const n8nIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,13 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class OpenaiDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
export class OpenaiDto extends BaseChatbotDto {
|
||||
openaiCredsId: string;
|
||||
botType?: string;
|
||||
botType: string;
|
||||
assistantId?: string;
|
||||
functionUrl?: string;
|
||||
model?: string;
|
||||
@ -17,35 +15,10 @@ export class OpenaiDto {
|
||||
assistantMessages?: string[];
|
||||
userMessages?: string[];
|
||||
maxTokens?: number;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class OpenaiSettingDto {
|
||||
export class OpenaiSettingDto extends BaseChatbotSettingDto {
|
||||
openaiCredsId?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
openaiIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
speechToText?: boolean;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance),
|
||||
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
@ -8,13 +7,12 @@ import { Events } from '@api/types/wa.types';
|
||||
import { configService, Typebot } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Typebot as TypebotModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class TypebotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class TypebotController extends BaseChatbotController<TypebotModel, TypebotDto> {
|
||||
constructor(
|
||||
private readonly typebotService: TypebotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@ -28,6 +26,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('TypebotController');
|
||||
protected readonly integrationName = 'Typebot';
|
||||
|
||||
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
|
||||
botRepository: any;
|
||||
@ -35,245 +34,35 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating typebot');
|
||||
}
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.typebotIdFallback;
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'typebotIdFallback';
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
protected getIntegrationType(): string {
|
||||
return 'typebot';
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error(
|
||||
'You already have a typebot with an "All" trigger, you cannot have more bots while it is active',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
@ -288,263 +77,24 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating typebot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { typebot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting typebot');
|
||||
}
|
||||
// Process Typebot-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: TypebotModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Use the simplified service method that follows the base class pattern
|
||||
await this.typebotService.processTypebot(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
typebotIdFallback: updateSettings.typebotIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
typebotIdFallback: newSetttings.typebotIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
typebotIdFallback: null,
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
typebotIdFallback: settings.typebotIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
// TypeBot specific method for starting a bot from API
|
||||
public async startBot(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
@ -552,7 +102,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
id: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -661,22 +211,25 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
},
|
||||
});
|
||||
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
null,
|
||||
null,
|
||||
findBot,
|
||||
url,
|
||||
// Use the simplified service method instead of the complex one
|
||||
const settings = {
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
};
|
||||
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
findBot,
|
||||
null, // session
|
||||
settings,
|
||||
'init',
|
||||
null, // pushName
|
||||
prefilledVariables,
|
||||
);
|
||||
} else {
|
||||
@ -722,7 +275,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
@ -747,325 +300,4 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
instanceId: instanceId,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
instanceId: instanceId,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
|
||||
}
|
||||
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const typebotData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
|
||||
|
||||
return { typebot: { ...instance, typebot: typebotData } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (typebot && typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: botId ?? { not: null },
|
||||
type: 'typebot',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instance,
|
||||
remoteJid,
|
||||
msg,
|
||||
}: {
|
||||
instance: InstanceDto;
|
||||
remoteJid: string;
|
||||
msg: any;
|
||||
pushName?: string;
|
||||
}) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!instanceData) throw new Error('Instance not found');
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as TypebotModel;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.typebotIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.typebotIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await this.prismaRepository.typebotSetting.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const url = findBot?.url;
|
||||
const typebot = findBot?.typebot;
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
|
||||
if (this.checkIgnoreJids(ignoreJids, remoteJid)) return;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.sessionRepository.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debouncedContent,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class PrefilledVariables {
|
||||
remoteJid?: string;
|
||||
@ -7,34 +7,11 @@ export class PrefilledVariables {
|
||||
additionalData?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class TypebotDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
export class TypebotDto extends BaseChatbotDto {
|
||||
url: string;
|
||||
typebot?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
typebot: string;
|
||||
}
|
||||
|
||||
export class TypebotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class TypebotSettingDto extends BaseChatbotSettingDto {
|
||||
typebotIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -132,6 +132,7 @@ export class EventController {
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
@ -151,5 +152,8 @@ export class EventController {
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'REMOVE_INSTANCE',
|
||||
'LOGOUT_INSTANCE',
|
||||
'INSTANCE_CREATE',
|
||||
'INSTANCE_DELETE',
|
||||
'STATUS_INSTANCE',
|
||||
];
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ export class EventDto {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
nats?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
pusher?: {
|
||||
enabled?: boolean;
|
||||
appId?: string;
|
||||
@ -63,6 +68,11 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
nats?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
pusher?: {
|
||||
enabled?: boolean;
|
||||
appId?: string;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { NatsController } from '@api/integrations/event/nats/nats.controller';
|
||||
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
|
||||
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
|
||||
import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
|
||||
@ -13,6 +14,7 @@ export class EventManager {
|
||||
private websocketController: WebsocketController;
|
||||
private webhookController: WebhookController;
|
||||
private rabbitmqController: RabbitmqController;
|
||||
private natsController: NatsController;
|
||||
private sqsController: SqsController;
|
||||
private pusherController: PusherController;
|
||||
|
||||
@ -23,6 +25,7 @@ export class EventManager {
|
||||
this.websocket = new WebsocketController(prismaRepository, waMonitor);
|
||||
this.webhook = new WebhookController(prismaRepository, waMonitor);
|
||||
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
|
||||
this.nats = new NatsController(prismaRepository, waMonitor);
|
||||
this.sqs = new SqsController(prismaRepository, waMonitor);
|
||||
this.pusher = new PusherController(prismaRepository, waMonitor);
|
||||
}
|
||||
@ -67,6 +70,14 @@ export class EventManager {
|
||||
return this.rabbitmqController;
|
||||
}
|
||||
|
||||
public set nats(nats: NatsController) {
|
||||
this.natsController = nats;
|
||||
}
|
||||
|
||||
public get nats() {
|
||||
return this.natsController;
|
||||
}
|
||||
|
||||
public set sqs(sqs: SqsController) {
|
||||
this.sqsController = sqs;
|
||||
}
|
||||
@ -85,6 +96,7 @@ export class EventManager {
|
||||
public init(httpServer: Server): void {
|
||||
this.websocket.init(httpServer);
|
||||
this.rabbitmq.init();
|
||||
this.nats.init();
|
||||
this.sqs.init();
|
||||
this.pusher.init();
|
||||
}
|
||||
@ -103,6 +115,7 @@ export class EventManager {
|
||||
}): Promise<void> {
|
||||
await this.websocket.emit(eventData);
|
||||
await this.rabbitmq.emit(eventData);
|
||||
await this.nats.emit(eventData);
|
||||
await this.sqs.emit(eventData);
|
||||
await this.webhook.emit(eventData);
|
||||
await this.pusher.emit(eventData);
|
||||
@ -125,6 +138,14 @@ export class EventManager {
|
||||
},
|
||||
});
|
||||
|
||||
if (data.nats)
|
||||
await this.nats.set(instanceName, {
|
||||
nats: {
|
||||
enabled: true,
|
||||
events: data.nats?.events,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.sqs)
|
||||
await this.sqs.set(instanceName, {
|
||||
sqs: {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
|
||||
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
|
||||
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
|
||||
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
|
||||
@ -14,6 +15,7 @@ export class EventRouter {
|
||||
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
|
||||
this.router.use('/websocket', new WebsocketRouter(...guards).router);
|
||||
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
|
||||
this.router.use('/nats', new NatsRouter(...guards).router);
|
||||
this.router.use('/pusher', new PusherRouter(...guards).router);
|
||||
this.router.use('/sqs', new SqsRouter(...guards).router);
|
||||
}
|
||||
|
@ -16,6 +16,9 @@ export const eventSchema: JSONSchema7 = {
|
||||
rabbitmq: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
nats: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
sqs: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
|
161
src/api/integrations/event/nats/nats.controller.ts
Normal file
161
src/api/integrations/event/nats/nats.controller.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Log, Nats } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { connect, NatsConnection, StringCodec } from 'nats';
|
||||
|
||||
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||
|
||||
export class NatsController extends EventController implements EventControllerInterface {
|
||||
public natsClient: NatsConnection | null = null;
|
||||
private readonly logger = new Logger('NatsController');
|
||||
private readonly sc = StringCodec();
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor, configService.get<Nats>('NATS')?.ENABLED, 'nats');
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
if (!this.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const uri = configService.get<Nats>('NATS').URI;
|
||||
|
||||
this.natsClient = await connect({ servers: uri });
|
||||
|
||||
this.logger.info('NATS initialized');
|
||||
|
||||
if (configService.get<Nats>('NATS')?.GLOBAL_ENABLED) {
|
||||
await this.initGlobalSubscriptions();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to connect to NATS:');
|
||||
this.logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instanceName,
|
||||
origin,
|
||||
event,
|
||||
data,
|
||||
serverUrl,
|
||||
dateTime,
|
||||
sender,
|
||||
apiKey,
|
||||
integration,
|
||||
}: EmitData): Promise<void> {
|
||||
if (integration && !integration.includes('nats')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.status || !this.natsClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceNats = await this.get(instanceName);
|
||||
const natsLocal = instanceNats?.events;
|
||||
const natsGlobal = configService.get<Nats>('NATS').GLOBAL_ENABLED;
|
||||
const natsEvents = configService.get<Nats>('NATS').EVENTS;
|
||||
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
|
||||
const we = event.replace(/[.-]/gm, '_').toUpperCase();
|
||||
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||
|
||||
const message = {
|
||||
event,
|
||||
instance: instanceName,
|
||||
data,
|
||||
server_url: serverUrl,
|
||||
date_time: dateTime,
|
||||
sender,
|
||||
apikey: apiKey,
|
||||
};
|
||||
|
||||
// Instância específica
|
||||
if (instanceNats?.enabled) {
|
||||
if (Array.isArray(natsLocal) && natsLocal.includes(we)) {
|
||||
const subject = `${instanceName}.${event.toLowerCase()}`;
|
||||
|
||||
try {
|
||||
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
|
||||
|
||||
if (logEnabled) {
|
||||
const logData = {
|
||||
local: `${origin}.sendData-NATS`,
|
||||
...message,
|
||||
};
|
||||
this.logger.log(logData);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish to NATS (instance): ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global
|
||||
if (natsGlobal && natsEvents[we]) {
|
||||
try {
|
||||
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
|
||||
|
||||
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
|
||||
|
||||
if (logEnabled) {
|
||||
const logData = {
|
||||
local: `${origin}.sendData-NATS-Global`,
|
||||
...message,
|
||||
};
|
||||
this.logger.log(logData);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish to NATS (global): ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initGlobalSubscriptions(): Promise<void> {
|
||||
this.logger.info('Initializing global subscriptions');
|
||||
|
||||
const events = configService.get<Nats>('NATS').EVENTS;
|
||||
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
|
||||
|
||||
if (!events) {
|
||||
this.logger.warn('No events to initialize on NATS');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventKeys = Object.keys(events);
|
||||
|
||||
for (const event of eventKeys) {
|
||||
if (events[event] === false) continue;
|
||||
|
||||
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
|
||||
|
||||
// Criar uma subscription para cada evento
|
||||
try {
|
||||
const subscription = this.natsClient.subscribe(subject);
|
||||
this.logger.info(`Subscribed to: ${subject}`);
|
||||
|
||||
// Processar mensagens (exemplo básico)
|
||||
(async () => {
|
||||
for await (const msg of subscription) {
|
||||
try {
|
||||
const data = JSON.parse(this.sc.decode(msg.data));
|
||||
// Aqui você pode adicionar a lógica de processamento
|
||||
this.logger.debug(`Received message on ${subject}:`);
|
||||
this.logger.debug(data);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing message on ${subject}:`);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to ${subject}:`);
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/api/integrations/event/nats/nats.router.ts
Normal file
36
src/api/integrations/event/nats/nats.router.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { EventDto } from '@api/integrations/event/event.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { eventManager } from '@api/server.module';
|
||||
import { eventSchema, instanceSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class NatsRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EventDto>({
|
||||
request: req,
|
||||
schema: eventSchema,
|
||||
ClassRef: EventDto,
|
||||
execute: (instance, data) => eventManager.nats.set(instance.instanceName, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => eventManager.nats.get(instance.instanceName),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
@ -21,9 +21,21 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
|
||||
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
|
||||
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
|
||||
|
||||
amqp.connect(uri, (error, connection) => {
|
||||
const url = new URL(uri);
|
||||
const connectionOptions = {
|
||||
protocol: url.protocol.slice(0, -1),
|
||||
hostname: url.hostname,
|
||||
port: url.port || 5672,
|
||||
username: url.username || 'guest',
|
||||
password: url.password || 'guest',
|
||||
vhost: url.pathname.slice(1) || '/',
|
||||
frameMax: frameMax,
|
||||
};
|
||||
|
||||
amqp.connect(connectionOptions, (error, connection) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { SQS } from '@aws-sdk/client-sqs';
|
||||
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
|
||||
import { configService, Log, Sqs } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||
import { EventDto } from '../event.dto';
|
||||
|
||||
export class SqsController extends EventController implements EventControllerInterface {
|
||||
private sqs: SQS;
|
||||
@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt
|
||||
return this.sqs;
|
||||
}
|
||||
|
||||
override async set(instanceName: string, data: EventDto): Promise<any> {
|
||||
if (!this.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data[this.name]?.enabled) {
|
||||
data[this.name].events = [];
|
||||
} else {
|
||||
if (0 === data[this.name].events.length) {
|
||||
data[this.name].events = EventController.events;
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled);
|
||||
|
||||
const payload: any = {
|
||||
where: {
|
||||
instanceId: this.monitor.waInstances[instanceName].instanceId,
|
||||
},
|
||||
update: {
|
||||
enabled: data[this.name]?.enabled,
|
||||
events: data[this.name].events,
|
||||
},
|
||||
create: {
|
||||
enabled: data[this.name]?.enabled,
|
||||
events: data[this.name].events,
|
||||
instanceId: this.monitor.waInstances[instanceName].instanceId,
|
||||
},
|
||||
};
|
||||
console.log('*** payload: ', payload);
|
||||
return this.prisma[this.name].upsert(payload);
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instanceName,
|
||||
origin,
|
||||
@ -121,70 +155,92 @@ export class SqsController extends EventController implements EventControllerInt
|
||||
}
|
||||
}
|
||||
|
||||
public async initQueues(instanceName: string, events: string[]) {
|
||||
if (!events || !events.length) return;
|
||||
private async saveQueues(instanceName: string, events: string[], enable: boolean) {
|
||||
if (enable) {
|
||||
const eventsFinded = await this.listQueuesByInstance(instanceName);
|
||||
console.log('eventsFinded', eventsFinded);
|
||||
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
for (const event of events) {
|
||||
const normalizedEvent = event.toLowerCase();
|
||||
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
if (eventsFinded.includes(normalizedEvent)) {
|
||||
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.sqs.createQueue(
|
||||
{
|
||||
QueueName: queueName,
|
||||
Attributes: {
|
||||
FifoQueue: 'true',
|
||||
},
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error creating queue ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
const queueName = `${instanceName}_${normalizedEvent}.fifo`;
|
||||
|
||||
try {
|
||||
const createCommand = new CreateQueueCommand({
|
||||
QueueName: queueName,
|
||||
Attributes: {
|
||||
FifoQueue: 'true',
|
||||
},
|
||||
});
|
||||
const data = await this.sqs.send(createCommand);
|
||||
this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async removeQueues(instanceName: string, events: any) {
|
||||
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
|
||||
if (!events || !eventsArray.length) return;
|
||||
private async listQueuesByInstance(instanceName: string) {
|
||||
let existingQueues: string[] = [];
|
||||
try {
|
||||
const listCommand = new ListQueuesCommand({
|
||||
QueueNamePrefix: `${instanceName}_`,
|
||||
});
|
||||
const listData = await this.sqs.send(listCommand);
|
||||
if (listData.QueueUrls && listData.QueueUrls.length > 0) {
|
||||
// Extrai o nome da fila a partir da URL
|
||||
existingQueues = listData.QueueUrls.map((queueUrl) => {
|
||||
const parts = queueUrl.split('/');
|
||||
return parts[parts.length - 1];
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const queues = eventsArray.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
// Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
|
||||
return existingQueues
|
||||
.map((queueName) => {
|
||||
// Espera-se que o nome seja `${instanceName}_${event}.fifo`
|
||||
if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) {
|
||||
return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase();
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter((event) => event !== '');
|
||||
}
|
||||
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
// Para uma futura feature de exclusão forçada das queues
|
||||
private async removeQueuesByInstance(instanceName: string) {
|
||||
try {
|
||||
const listCommand = new ListQueuesCommand({
|
||||
QueueNamePrefix: `${instanceName}_`,
|
||||
});
|
||||
const listData = await this.sqs.send(listCommand);
|
||||
|
||||
this.sqs.getQueueUrl(
|
||||
{
|
||||
QueueName: queueName,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
const queueUrl = data.QueueUrl;
|
||||
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
|
||||
this.logger.info(`No queues found for instance ${instanceName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqs.deleteQueue(
|
||||
{
|
||||
QueueUrl: queueUrl,
|
||||
},
|
||||
(deleteErr) => {
|
||||
if (deleteErr) {
|
||||
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
|
||||
} else {
|
||||
this.logger.info(`Queue ${queueName} deleted`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
for (const queueUrl of listData.QueueUrls) {
|
||||
try {
|
||||
const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
|
||||
await this.sqs.send(deleteCommand);
|
||||
this.logger.info(`Queue ${queueUrl} deleted`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { configService, Log, Webhook } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { isURL } from 'class-validator';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
|
||||
|
||||
@ -18,7 +18,7 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
|
||||
if (!isURL(data.webhook.url, { require_tld: false })) {
|
||||
if (!/^(https?:\/\/)/.test(data.webhook.url)) {
|
||||
throw new BadRequestException('Invalid "url" property');
|
||||
}
|
||||
|
||||
@ -74,10 +74,20 @@ export class WebhookController extends EventController implements EventControlle
|
||||
|
||||
const webhookConfig = configService.get<Webhook>('WEBHOOK');
|
||||
const webhookLocal = instance?.events;
|
||||
const webhookHeaders = instance?.headers;
|
||||
const webhookHeaders = { ...((instance?.headers as Record<string, string>) || {}) };
|
||||
|
||||
if (webhookHeaders && 'jwt_key' in webhookHeaders) {
|
||||
const jwtKey = webhookHeaders['jwt_key'];
|
||||
const jwtToken = this.generateJwtToken(jwtKey);
|
||||
webhookHeaders['Authorization'] = `Bearer ${jwtToken}`;
|
||||
|
||||
delete webhookHeaders['jwt_key'];
|
||||
}
|
||||
|
||||
const we = event.replace(/[.-]/gm, '_').toUpperCase();
|
||||
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
|
||||
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
|
||||
const regex = /^(https?:\/\/)/;
|
||||
|
||||
const webhookData = {
|
||||
event,
|
||||
@ -111,10 +121,11 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
|
||||
if (instance?.enabled && regex.test(instance.url)) {
|
||||
const httpService = axios.create({
|
||||
baseURL,
|
||||
headers: webhookHeaders as Record<string, string> | undefined,
|
||||
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||
});
|
||||
|
||||
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
|
||||
@ -155,8 +166,11 @@ export class WebhookController extends EventController implements EventControlle
|
||||
}
|
||||
|
||||
try {
|
||||
if (isURL(globalURL)) {
|
||||
const httpService = axios.create({ baseURL: globalURL });
|
||||
if (regex.test(globalURL)) {
|
||||
const httpService = axios.create({
|
||||
baseURL: globalURL,
|
||||
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||
});
|
||||
|
||||
await this.retryWebhookRequest(
|
||||
httpService,
|
||||
@ -190,12 +204,20 @@ export class WebhookController extends EventController implements EventControlle
|
||||
origin: string,
|
||||
baseURL: string,
|
||||
serverUrl: string,
|
||||
maxRetries = 10,
|
||||
delaySeconds = 30,
|
||||
maxRetries?: number,
|
||||
delaySeconds?: number,
|
||||
): Promise<void> {
|
||||
const webhookConfig = configService.get<Webhook>('WEBHOOK');
|
||||
const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10;
|
||||
const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5;
|
||||
const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true;
|
||||
const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300;
|
||||
const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2;
|
||||
const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422];
|
||||
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxRetries) {
|
||||
while (attempts < maxRetryAttempts) {
|
||||
try {
|
||||
await httpService.post('', webhookData);
|
||||
if (attempts > 0) {
|
||||
@ -209,12 +231,27 @@ export class WebhookController extends EventController implements EventControlle
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
|
||||
const isTimeout = error.code === 'ECONNABORTED';
|
||||
|
||||
if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) {
|
||||
this.logger.error({
|
||||
local: `${origin}`,
|
||||
message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`,
|
||||
statusCode: error?.response?.status,
|
||||
url: baseURL,
|
||||
server_url: serverUrl,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
local: `${origin}`,
|
||||
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
|
||||
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
|
||||
hostName: error?.hostname,
|
||||
syscall: error?.syscall,
|
||||
code: error?.code,
|
||||
isTimeout,
|
||||
statusCode: error?.response?.status,
|
||||
error: error?.errno,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
@ -222,12 +259,46 @@ export class WebhookController extends EventController implements EventControlle
|
||||
server_url: serverUrl,
|
||||
});
|
||||
|
||||
if (attempts === maxRetries) {
|
||||
if (attempts === maxRetryAttempts) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
|
||||
let nextDelay = initialDelay;
|
||||
if (useExponentialBackoff) {
|
||||
nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay);
|
||||
|
||||
const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1);
|
||||
nextDelay = Math.max(initialDelay, nextDelay + jitter);
|
||||
}
|
||||
|
||||
this.logger.log({
|
||||
local: `${origin}`,
|
||||
message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`,
|
||||
url: baseURL,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generateJwtToken(authToken: string): string {
|
||||
try {
|
||||
const payload = {
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration
|
||||
app: 'evolution',
|
||||
action: 'webhook',
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, authToken, { algorithm: 'HS256' });
|
||||
return token;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
local: 'WebhookController.generateJwtToken',
|
||||
message: `JWT generation failed: ${error?.message}`,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Cors, Log, Websocket } from '@config/env.config';
|
||||
import { Auth, configService, Cors, Log, Websocket } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Server } from 'http';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl
|
||||
}
|
||||
|
||||
this.socket = new SocketIO(httpServer, {
|
||||
cors: {
|
||||
origin: this.cors,
|
||||
cors: { origin: this.cors },
|
||||
allowRequest: async (req, callback) => {
|
||||
try {
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const params = new URLSearchParams(url.search);
|
||||
|
||||
// Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4)
|
||||
if (params.has('EIO')) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
const apiKey = params.get('apikey') || (req.headers.apikey as string);
|
||||
|
||||
if (!apiKey) {
|
||||
this.logger.error('Connection rejected: apiKey not provided');
|
||||
return callback('apiKey is required', false);
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } });
|
||||
|
||||
if (!instance) {
|
||||
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
|
||||
if (apiKey !== globalToken) {
|
||||
this.logger.error('Connection rejected: invalid global token');
|
||||
return callback('Invalid global token', false);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
} catch (error) {
|
||||
this.logger.error('Authentication error:');
|
||||
this.logger.error(error);
|
||||
callback('Authentication error', false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -101,10 +133,7 @@ export class WebsocketController extends EventController implements EventControl
|
||||
this.socket.emit(event, message);
|
||||
|
||||
if (logEnabled) {
|
||||
this.logger.log({
|
||||
local: `${origin}.sendData-WebsocketGlobal`,
|
||||
...message,
|
||||
});
|
||||
this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl
|
||||
this.socket.of(`/${instanceName}`).emit(event, message);
|
||||
|
||||
if (logEnabled) {
|
||||
this.logger.log({
|
||||
local: `${origin}.sendData-Websocket`,
|
||||
...message,
|
||||
});
|
||||
this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -63,9 +63,9 @@ const createBucket = async () => {
|
||||
if (!exists) {
|
||||
await minioClient.makeBucket(bucketName);
|
||||
}
|
||||
|
||||
await setBucketPolicy();
|
||||
|
||||
if (!BUCKET.SKIP_POLICY) {
|
||||
await setBucketPolicy();
|
||||
}
|
||||
logger.info(`S3 Bucket ${bucketName} - ON`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Auth, ConfigService, ProviderSession } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import axios from 'axios';
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
type ResponseSuccess = { status: number; data?: any };
|
||||
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
|
||||
@ -36,7 +36,7 @@ export class ProviderFiles {
|
||||
} catch (error) {
|
||||
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
|
||||
const pid = process.pid;
|
||||
execSync(`kill -9 ${pid}`);
|
||||
execFileSync('kill', ['-9', `${pid}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
src/api/routes/business.router.ts
Normal file
37
src/api/routes/business.router.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { NumberDto } from '@api/dto/chat.dto';
|
||||
import { businessController } from '@api/server.module';
|
||||
import { catalogSchema, collectionsSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { HttpStatus } from './index.router';
|
||||
|
||||
export class BusinessRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('getCatalog'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<NumberDto>({
|
||||
request: req,
|
||||
schema: catalogSchema,
|
||||
ClassRef: NumberDto,
|
||||
execute: (instance, data) => businessController.fetchCatalog(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
|
||||
.post(this.routerPath('getCollections'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<NumberDto>({
|
||||
request: req,
|
||||
schema: collectionsSchema,
|
||||
ClassRef: NumberDto,
|
||||
execute: (instance, data) => businessController.fetchCollections(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
@ -207,7 +207,6 @@ export class ChatRouter extends RouterBroker {
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
|
||||
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ProfileNameDto>({
|
||||
request: req,
|
||||
|
@ -11,6 +11,7 @@ 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';
|
||||
@ -82,6 +83,7 @@ router
|
||||
.use('/message', new MessageRouter(...guards).router)
|
||||
.use('/call', new CallRouter(...guards).router)
|
||||
.use('/chat', new ChatRouter(...guards).router)
|
||||
.use('/business', new BusinessRouter(...guards).router)
|
||||
.use('/group', new GroupRouter(...guards).router)
|
||||
.use('/template', new TemplateRouter(configService, ...guards).router)
|
||||
.use('/settings', new SettingsRouter(...guards).router)
|
||||
|
@ -15,7 +15,6 @@ export class InstanceRouter extends RouterBroker {
|
||||
super();
|
||||
this.router
|
||||
.post('/create', ...guards, async (req, res) => {
|
||||
console.log('create instance', req.body);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
|
@ -3,6 +3,7 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
|
||||
import { eventEmitter } from '@config/event.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
import { BusinessController } from './controllers/business.controller';
|
||||
import { CallController } from './controllers/call.controller';
|
||||
import { ChatController } from './controllers/chat.controller';
|
||||
import { GroupController } from './controllers/group.controller';
|
||||
@ -21,10 +22,14 @@ import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/
|
||||
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
|
||||
import { DifyService } from './integrations/chatbot/dify/services/dify.service';
|
||||
import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller';
|
||||
import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service';
|
||||
import { EvolutionBotController } from './integrations/chatbot/evolutionBot/controllers/evolutionBot.controller';
|
||||
import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service';
|
||||
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
|
||||
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
|
||||
import { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
|
||||
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
|
||||
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
|
||||
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
|
||||
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
|
||||
@ -98,6 +103,7 @@ export const instanceController = new InstanceController(
|
||||
export const sendMessageController = new SendMessageController(waMonitor);
|
||||
export const callController = new CallController(waMonitor);
|
||||
export const chatController = new ChatController(waMonitor);
|
||||
export const businessController = new BusinessController(waMonitor);
|
||||
export const groupController = new GroupController(waMonitor);
|
||||
export const labelController = new LabelController(waMonitor);
|
||||
|
||||
@ -109,20 +115,27 @@ export const channelController = new ChannelController(prismaRepository, waMonit
|
||||
export const evolutionController = new EvolutionController(prismaRepository, waMonitor);
|
||||
export const metaController = new MetaController(prismaRepository, waMonitor);
|
||||
export const baileysController = new BaileysController(waMonitor);
|
||||
// chatbots
|
||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
|
||||
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
|
||||
|
||||
const openaiService = new OpenaiService(waMonitor, configService, prismaRepository);
|
||||
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
|
||||
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
|
||||
|
||||
const difyService = new DifyService(waMonitor, configService, prismaRepository);
|
||||
// chatbots
|
||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
|
||||
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
|
||||
|
||||
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
|
||||
|
||||
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository);
|
||||
const evolutionBotService = new EvolutionBotService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
|
||||
|
||||
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository);
|
||||
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
|
||||
|
||||
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
|
||||
|
||||
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor);
|
||||
|
||||
logger.info('Module - ON');
|
||||
|
@ -45,11 +45,11 @@ export class ChannelStartupService {
|
||||
this.chatwootCache,
|
||||
);
|
||||
|
||||
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository);
|
||||
public openaiService = new OpenaiService(waMonitor, this.prismaRepository, this.configService);
|
||||
|
||||
public openaiService = new OpenaiService(waMonitor, this.configService, this.prismaRepository);
|
||||
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
|
||||
|
||||
public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository);
|
||||
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceName);
|
||||
@ -503,8 +503,29 @@ export class ChannelStartupService {
|
||||
where['remoteJid'] = remoteJid;
|
||||
}
|
||||
|
||||
return await this.prismaRepository.contact.findMany({
|
||||
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
||||
where,
|
||||
};
|
||||
|
||||
if (query.offset) contactFindManyArgs.take = query.offset;
|
||||
if (query.page) {
|
||||
const validPage = Math.max(query.page as number, 1);
|
||||
contactFindManyArgs.skip = query.offset * (validPage - 1);
|
||||
}
|
||||
|
||||
const contacts = await this.prismaRepository.contact.findMany(contactFindManyArgs);
|
||||
|
||||
return contacts.map((contact) => {
|
||||
const remoteJid = contact.remoteJid;
|
||||
const isGroup = remoteJid.endsWith('@g.us');
|
||||
const isSaved = !!contact.pushName || !!contact.profilePicUrl;
|
||||
const type = isGroup ? 'group' : isSaved ? 'contact' : 'group_member';
|
||||
return {
|
||||
...contact,
|
||||
isGroup,
|
||||
isSaved,
|
||||
type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -685,88 +706,100 @@ export class ChannelStartupService {
|
||||
const timestampFilter =
|
||||
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
|
||||
? Prisma.sql`
|
||||
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
|
||||
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
|
||||
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
|
||||
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
|
||||
: Prisma.sql``;
|
||||
|
||||
const limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``;
|
||||
const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : Prisma.sql``;
|
||||
|
||||
const results = await this.prismaRepository.$queryRaw`
|
||||
WITH rankedMessages AS (
|
||||
SELECT DISTINCT ON ("Contact"."remoteJid")
|
||||
"Contact"."id",
|
||||
"Contact"."remoteJid",
|
||||
"Contact"."pushName",
|
||||
"Contact"."profilePicUrl",
|
||||
COALESCE(
|
||||
to_timestamp("Message"."messageTimestamp"::double precision),
|
||||
"Contact"."updatedAt"
|
||||
) as "updatedAt",
|
||||
"Chat"."createdAt" as "windowStart",
|
||||
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
|
||||
CASE
|
||||
WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true
|
||||
ELSE false
|
||||
END as "windowActive",
|
||||
"Message"."id" AS lastMessageId,
|
||||
"Message"."key" AS lastMessage_key,
|
||||
"Message"."pushName" AS lastMessagePushName,
|
||||
"Message"."participant" AS lastMessageParticipant,
|
||||
"Message"."messageType" AS lastMessageMessageType,
|
||||
"Message"."message" AS lastMessageMessage,
|
||||
"Message"."contextInfo" AS lastMessageContextInfo,
|
||||
"Message"."source" AS lastMessageSource,
|
||||
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
|
||||
"Message"."instanceId" AS lastMessageInstanceId,
|
||||
"Message"."sessionId" AS lastMessageSessionId,
|
||||
"Message"."status" AS lastMessageStatus
|
||||
FROM "Contact"
|
||||
INNER JOIN "Message" ON "Message"."key"->>'remoteJid' = "Contact"."remoteJid"
|
||||
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Contact"."remoteJid"
|
||||
AND "Chat"."instanceId" = "Contact"."instanceId"
|
||||
WHERE
|
||||
"Contact"."instanceId" = ${this.instanceId}
|
||||
AND "Message"."instanceId" = ${this.instanceId}
|
||||
${remoteJid ? Prisma.sql`AND "Contact"."remoteJid" = ${remoteJid}` : Prisma.sql``}
|
||||
${timestampFilter}
|
||||
ORDER BY
|
||||
"Contact"."remoteJid",
|
||||
"Message"."messageTimestamp" DESC
|
||||
)
|
||||
SELECT * FROM rankedMessages
|
||||
ORDER BY "updatedAt" DESC NULLS LAST;
|
||||
WITH rankedMessages AS (
|
||||
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
|
||||
"Contact"."id" as "contactId",
|
||||
"Message"."key"->>'remoteJid' as "remoteJid",
|
||||
CASE
|
||||
WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName")
|
||||
ELSE COALESCE("Contact"."pushName", "Message"."pushName")
|
||||
END as "pushName",
|
||||
"Contact"."profilePicUrl",
|
||||
COALESCE(
|
||||
to_timestamp("Message"."messageTimestamp"::double precision),
|
||||
"Contact"."updatedAt"
|
||||
) as "updatedAt",
|
||||
"Chat"."name" as "pushName",
|
||||
"Chat"."createdAt" as "windowStart",
|
||||
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
|
||||
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
|
||||
"Message"."id" AS lastMessageId,
|
||||
"Message"."key" AS lastMessage_key,
|
||||
CASE
|
||||
WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você'
|
||||
ELSE "Message"."pushName"
|
||||
END AS lastMessagePushName,
|
||||
"Message"."participant" AS lastMessageParticipant,
|
||||
"Message"."messageType" AS lastMessageMessageType,
|
||||
"Message"."message" AS lastMessageMessage,
|
||||
"Message"."contextInfo" AS lastMessageContextInfo,
|
||||
"Message"."source" AS lastMessageSource,
|
||||
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
|
||||
"Message"."instanceId" AS lastMessageInstanceId,
|
||||
"Message"."sessionId" AS lastMessageSessionId,
|
||||
"Message"."status" AS lastMessageStatus
|
||||
FROM "Message"
|
||||
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
|
||||
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
|
||||
WHERE "Message"."instanceId" = ${this.instanceId}
|
||||
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
|
||||
${timestampFilter}
|
||||
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
|
||||
)
|
||||
SELECT * FROM rankedMessages
|
||||
ORDER BY "updatedAt" DESC NULLS LAST
|
||||
${limit}
|
||||
${offset};
|
||||
`;
|
||||
|
||||
if (results && isArray(results) && results.length > 0) {
|
||||
const mappedResults = results.map((contact) => {
|
||||
const lastMessage = contact.lastMessageId
|
||||
const lastMessage = contact.lastmessageid
|
||||
? {
|
||||
id: contact.lastMessageId,
|
||||
key: contact.lastMessageKey,
|
||||
pushName: contact.lastMessagePushName,
|
||||
participant: contact.lastMessageParticipant,
|
||||
messageType: contact.lastMessageMessageType,
|
||||
message: contact.lastMessageMessage,
|
||||
contextInfo: contact.lastMessageContextInfo,
|
||||
source: contact.lastMessageSource,
|
||||
messageTimestamp: contact.lastMessageMessageTimestamp,
|
||||
instanceId: contact.lastMessageInstanceId,
|
||||
sessionId: contact.lastMessageSessionId,
|
||||
status: contact.lastMessageStatus,
|
||||
id: contact.lastmessageid,
|
||||
key: contact.lastmessage_key,
|
||||
pushName: contact.lastmessagepushname,
|
||||
participant: contact.lastmessageparticipant,
|
||||
messageType: contact.lastmessagemessagetype,
|
||||
message: contact.lastmessagemessage,
|
||||
contextInfo: contact.lastmessagecontextinfo,
|
||||
source: contact.lastmessagesource,
|
||||
messageTimestamp: contact.lastmessagemessagetimestamp,
|
||||
instanceId: contact.lastmessageinstanceid,
|
||||
sessionId: contact.lastmessagesessionid,
|
||||
status: contact.lastmessagestatus,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: contact.id,
|
||||
remoteJid: contact.remoteJid,
|
||||
pushName: contact.pushName,
|
||||
profilePicUrl: contact.profilePicUrl,
|
||||
updatedAt: contact.updatedAt,
|
||||
windowStart: contact.windowStart,
|
||||
windowExpires: contact.windowExpires,
|
||||
windowActive: contact.windowActive,
|
||||
id: contact.contactid || null,
|
||||
remoteJid: contact.remotejid,
|
||||
pushName: contact.pushname,
|
||||
profilePicUrl: contact.profilepicurl,
|
||||
updatedAt: contact.updatedat,
|
||||
windowStart: contact.windowstart,
|
||||
windowExpires: contact.windowexpires,
|
||||
windowActive: contact.windowactive,
|
||||
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
|
||||
unreadCount: 0,
|
||||
isSaved: !!contact.contactid,
|
||||
};
|
||||
});
|
||||
|
||||
if (query?.take && query?.skip) {
|
||||
const skip = query.skip || 0;
|
||||
const take = query.take || 20;
|
||||
return mappedResults.slice(skip, skip + take);
|
||||
}
|
||||
|
||||
return mappedResults;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
|
||||
import { NotFoundException } from '@exceptions';
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync } from 'child_process';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
@ -91,6 +91,7 @@ export class WAMonitoringService {
|
||||
Chatwoot: true,
|
||||
Proxy: true,
|
||||
Rabbitmq: true,
|
||||
Nats: true,
|
||||
Sqs: true,
|
||||
Websocket: true,
|
||||
Setting: true,
|
||||
@ -168,7 +169,8 @@ export class WAMonitoringService {
|
||||
|
||||
public async cleaningStoreData(instanceName: string) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
|
||||
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
|
||||
execFileSync('rm', ['-rf', instancePath]);
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({
|
||||
@ -190,6 +192,7 @@ export class WAMonitoringService {
|
||||
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });
|
||||
|
@ -15,6 +15,7 @@ export enum Events {
|
||||
MESSAGES_UPDATE = 'messages.update',
|
||||
MESSAGES_DELETE = 'messages.delete',
|
||||
SEND_MESSAGE = 'send.message',
|
||||
SEND_MESSAGE_UPDATE = 'send.message.update',
|
||||
CONTACTS_SET = 'contacts.set',
|
||||
CONTACTS_UPSERT = 'contacts.upsert',
|
||||
CONTACTS_UPDATE = 'contacts.update',
|
||||
|
@ -72,6 +72,7 @@ export type EventsRabbitmq = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -94,10 +95,20 @@ export type EventsRabbitmq = {
|
||||
export type Rabbitmq = {
|
||||
ENABLED: boolean;
|
||||
URI: string;
|
||||
FRAME_MAX: number;
|
||||
EXCHANGE_NAME: string;
|
||||
GLOBAL_ENABLED: boolean;
|
||||
EVENTS: EventsRabbitmq;
|
||||
PREFIX_KEY: string;
|
||||
PREFIX_KEY?: string;
|
||||
};
|
||||
|
||||
export type Nats = {
|
||||
ENABLED: boolean;
|
||||
URI: string;
|
||||
EXCHANGE_NAME: string;
|
||||
GLOBAL_ENABLED: boolean;
|
||||
EVENTS: EventsRabbitmq;
|
||||
PREFIX_KEY?: string;
|
||||
};
|
||||
|
||||
export type Sqs = {
|
||||
@ -131,6 +142,7 @@ export type EventsWebhook = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -163,6 +175,7 @@ export type EventsPusher = {
|
||||
MESSAGES_UPDATE: boolean;
|
||||
MESSAGES_DELETE: boolean;
|
||||
SEND_MESSAGE: boolean;
|
||||
SEND_MESSAGE_UPDATE: boolean;
|
||||
CONTACTS_SET: boolean;
|
||||
CONTACTS_UPDATE: boolean;
|
||||
CONTACTS_UPSERT: boolean;
|
||||
@ -220,7 +233,21 @@ export type CacheConfLocal = {
|
||||
TTL: number;
|
||||
};
|
||||
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
|
||||
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
|
||||
export type Webhook = {
|
||||
GLOBAL?: GlobalWebhook;
|
||||
EVENTS: EventsWebhook;
|
||||
REQUEST?: {
|
||||
TIMEOUT_MS?: number;
|
||||
};
|
||||
RETRY?: {
|
||||
MAX_ATTEMPTS?: number;
|
||||
INITIAL_DELAY_SECONDS?: number;
|
||||
USE_EXPONENTIAL_BACKOFF?: boolean;
|
||||
MAX_DELAY_SECONDS?: number;
|
||||
JITTER_FACTOR?: number;
|
||||
NON_RETRYABLE_STATUS_CODES?: number[];
|
||||
};
|
||||
};
|
||||
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
||||
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
|
||||
export type QrCode = { LIMIT: number; COLOR: string };
|
||||
@ -241,6 +268,9 @@ export type Chatwoot = {
|
||||
};
|
||||
export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
|
||||
export type Dify = { ENABLED: boolean };
|
||||
export type N8n = { ENABLED: boolean };
|
||||
export type Evoai = { ENABLED: boolean };
|
||||
export type Flowise = { ENABLED: boolean };
|
||||
|
||||
export type S3 = {
|
||||
ACCESS_KEY: string;
|
||||
@ -251,6 +281,7 @@ export type S3 = {
|
||||
PORT?: number;
|
||||
USE_SSL?: boolean;
|
||||
REGION?: string;
|
||||
SKIP_POLICY?: boolean;
|
||||
};
|
||||
|
||||
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
|
||||
@ -263,6 +294,7 @@ export interface Env {
|
||||
PROVIDER: ProviderSession;
|
||||
DATABASE: Database;
|
||||
RABBITMQ: Rabbitmq;
|
||||
NATS: Nats;
|
||||
SQS: Sqs;
|
||||
WEBSOCKET: Websocket;
|
||||
WA_BUSINESS: WaBusiness;
|
||||
@ -278,6 +310,9 @@ export interface Env {
|
||||
CHATWOOT: Chatwoot;
|
||||
OPENAI: Openai;
|
||||
DIFY: Dify;
|
||||
N8N: N8n;
|
||||
EVOAI: Evoai;
|
||||
FLOWISE: Flowise;
|
||||
CACHE: CacheConf;
|
||||
S3?: S3;
|
||||
AUTHENTICATION: Auth;
|
||||
@ -356,9 +391,10 @@ export class ConfigService {
|
||||
RABBITMQ: {
|
||||
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
|
||||
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
|
||||
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || 'evolution',
|
||||
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY,
|
||||
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
|
||||
URI: process.env.RABBITMQ_URI || '',
|
||||
FRAME_MAX: Number.parseInt(process.env.RABBITMQ_FRAME_MAX) || 8192,
|
||||
EVENTS: {
|
||||
APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true',
|
||||
INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true',
|
||||
@ -370,6 +406,7 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -389,6 +426,43 @@ export class ConfigService {
|
||||
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
|
||||
},
|
||||
},
|
||||
NATS: {
|
||||
ENABLED: process.env?.NATS_ENABLED === 'true',
|
||||
GLOBAL_ENABLED: process.env?.NATS_GLOBAL_ENABLED === 'true',
|
||||
PREFIX_KEY: process.env?.NATS_PREFIX_KEY,
|
||||
EXCHANGE_NAME: process.env?.NATS_EXCHANGE_NAME || 'evolution_exchange',
|
||||
URI: process.env.NATS_URI || '',
|
||||
EVENTS: {
|
||||
APPLICATION_STARTUP: process.env?.NATS_EVENTS_APPLICATION_STARTUP === 'true',
|
||||
INSTANCE_CREATE: process.env?.NATS_EVENTS_INSTANCE_CREATE === 'true',
|
||||
INSTANCE_DELETE: process.env?.NATS_EVENTS_INSTANCE_DELETE === 'true',
|
||||
QRCODE_UPDATED: process.env?.NATS_EVENTS_QRCODE_UPDATED === 'true',
|
||||
MESSAGES_SET: process.env?.NATS_EVENTS_MESSAGES_SET === 'true',
|
||||
MESSAGES_UPSERT: process.env?.NATS_EVENTS_MESSAGES_UPSERT === 'true',
|
||||
MESSAGES_EDITED: process.env?.NATS_EVENTS_MESSAGES_EDITED === 'true',
|
||||
MESSAGES_UPDATE: process.env?.NATS_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.NATS_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.NATS_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.NATS_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.NATS_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.NATS_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.NATS_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
PRESENCE_UPDATE: process.env?.NATS_EVENTS_PRESENCE_UPDATE === 'true',
|
||||
CHATS_SET: process.env?.NATS_EVENTS_CHATS_SET === 'true',
|
||||
CHATS_UPDATE: process.env?.NATS_EVENTS_CHATS_UPDATE === 'true',
|
||||
CHATS_UPSERT: process.env?.NATS_EVENTS_CHATS_UPSERT === 'true',
|
||||
CHATS_DELETE: process.env?.NATS_EVENTS_CHATS_DELETE === 'true',
|
||||
CONNECTION_UPDATE: process.env?.NATS_EVENTS_CONNECTION_UPDATE === 'true',
|
||||
LABELS_EDIT: process.env?.NATS_EVENTS_LABELS_EDIT === 'true',
|
||||
LABELS_ASSOCIATION: process.env?.NATS_EVENTS_LABELS_ASSOCIATION === 'true',
|
||||
GROUPS_UPSERT: process.env?.NATS_EVENTS_GROUPS_UPSERT === 'true',
|
||||
GROUP_UPDATE: process.env?.NATS_EVENTS_GROUPS_UPDATE === 'true',
|
||||
GROUP_PARTICIPANTS_UPDATE: process.env?.NATS_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
|
||||
CALL: process.env?.NATS_EVENTS_CALL === 'true',
|
||||
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
|
||||
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
|
||||
},
|
||||
},
|
||||
SQS: {
|
||||
ENABLED: process.env?.SQS_ENABLED === 'true',
|
||||
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '',
|
||||
@ -421,6 +495,7 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.PUSHER_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -477,6 +552,7 @@ export class ConfigService {
|
||||
MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
|
||||
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
|
||||
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true',
|
||||
SEND_MESSAGE_UPDATE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE === 'true',
|
||||
CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true',
|
||||
CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true',
|
||||
CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true',
|
||||
@ -497,6 +573,19 @@ export class ConfigService {
|
||||
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
|
||||
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
|
||||
},
|
||||
REQUEST: {
|
||||
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
|
||||
},
|
||||
RETRY: {
|
||||
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
|
||||
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
|
||||
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
|
||||
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
|
||||
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
|
||||
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [
|
||||
400, 401, 403, 404, 422,
|
||||
],
|
||||
},
|
||||
},
|
||||
CONFIG_SESSION_PHONE: {
|
||||
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
||||
@ -533,6 +622,15 @@ export class ConfigService {
|
||||
DIFY: {
|
||||
ENABLED: process.env?.DIFY_ENABLED === 'true',
|
||||
},
|
||||
N8N: {
|
||||
ENABLED: process.env?.N8N_ENABLED === 'true',
|
||||
},
|
||||
EVOAI: {
|
||||
ENABLED: process.env?.EVOAI_ENABLED === 'true',
|
||||
},
|
||||
FLOWISE: {
|
||||
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
|
||||
},
|
||||
CACHE: {
|
||||
REDIS: {
|
||||
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
|
||||
@ -555,6 +653,7 @@ export class ConfigService {
|
||||
PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
|
||||
USE_SSL: process.env?.S3_USE_SSL === 'true',
|
||||
REGION: process.env?.S3_REGION,
|
||||
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
|
||||
},
|
||||
AUTHENTICATION: {
|
||||
API_KEY: {
|
||||
|
10
src/main.ts
10
src/main.ts
@ -128,7 +128,15 @@ async function bootstrap() {
|
||||
const httpServer = configService.get<HttpServer>('SERVER');
|
||||
|
||||
ServerUP.app = app;
|
||||
const server = ServerUP[httpServer.TYPE];
|
||||
let server = ServerUP[httpServer.TYPE];
|
||||
|
||||
if (server === null) {
|
||||
logger.warn('SSL cert load failed — falling back to HTTP.');
|
||||
logger.info("Ensure 'SSL_CONF_PRIVKEY' and 'SSL_CONF_FULLCHAIN' env vars point to valid certificate files.");
|
||||
|
||||
httpServer.TYPE = 'http';
|
||||
server = ServerUP[httpServer.TYPE];
|
||||
}
|
||||
|
||||
eventManager.init(server);
|
||||
|
||||
|
@ -3,19 +3,19 @@ import { configService, S3 } from '@config/env.config';
|
||||
const getTypeMessage = (msg: any) => {
|
||||
let mediaId: string;
|
||||
|
||||
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message.mediaUrl;
|
||||
else mediaId = msg.key.id;
|
||||
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message?.mediaUrl;
|
||||
else mediaId = msg.key?.id;
|
||||
|
||||
const types = {
|
||||
conversation: msg?.message?.conversation,
|
||||
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
|
||||
contactMessage: msg?.message?.contactMessage?.displayName,
|
||||
locationMessage: msg?.message?.locationMessage?.degreesLatitude,
|
||||
locationMessage: msg?.message?.locationMessage?.degreesLatitude.toString(),
|
||||
viewOnceMessageV2:
|
||||
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
|
||||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
|
||||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
|
||||
listResponseMessage: msg?.message?.listResponseMessage?.title,
|
||||
listResponseMessage: msg?.message?.listResponseMessage?.title || msg?.listResponseMessage?.title,
|
||||
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
|
||||
templateButtonReplyMessage:
|
||||
msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId,
|
||||
|
17
src/validate/business.schema.ts
Normal file
17
src/validate/business.schema.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
|
||||
export const catalogSchema: JSONSchema7 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
number: { type: 'string' },
|
||||
limit: { type: 'number' },
|
||||
},
|
||||
};
|
||||
|
||||
export const collectionsSchema: JSONSchema7 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
number: { type: 'string' },
|
||||
limit: { type: 'number' },
|
||||
},
|
||||
};
|
@ -68,6 +68,7 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
@ -104,6 +105,44 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
],
|
||||
},
|
||||
},
|
||||
// NATS
|
||||
natsEnabled: { type: 'boolean' },
|
||||
natsEvents: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_EDITED',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
@ -140,6 +179,7 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'SEND_MESSAGE_UPDATE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Integrations Schema
|
||||
export * from './business.schema';
|
||||
export * from './chat.schema';
|
||||
export * from './group.schema';
|
||||
export * from './instance.schema';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user