Compare commits

..

No commits in common. "main" and "2.2.0-pre" have entirely different histories.

136 changed files with 8780 additions and 22697 deletions

View File

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

View File

@ -3,9 +3,6 @@ SERVER_PORT=8080
# Server URL - Set your application url # Server URL - Set your application url
SERVER_URL=http://localhost:8080 SERVER_URL=http://localhost:8080
SSL_CONF_PRIVKEY=/path/to/cert.key
SSL_CONF_FULLCHAIN=/path/to/cert.crt
SENTRY_DSN= SENTRY_DSN=
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' # Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
@ -29,7 +26,7 @@ DEL_INSTANCE=false
# Provider: postgresql | mysql # Provider: postgresql | mysql
DATABASE_PROVIDER=postgresql DATABASE_PROVIDER=postgresql
DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution?schema=public' DATABASE_CONNECTION_URI='postgresql://user:pass@localhost:5432/evolution?schema=public'
# Client name for the database connection # Client name for the database connection
# It is used to separate an API installation from another that uses the same database. # It is used to separate an API installation from another that uses the same database.
DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange
@ -50,11 +47,8 @@ DATABASE_DELETE_MESSAGE=true
RABBITMQ_ENABLED=false RABBITMQ_ENABLED=false
RABBITMQ_URI=amqp://localhost RABBITMQ_URI=amqp://localhost
RABBITMQ_EXCHANGE_NAME=evolution RABBITMQ_EXCHANGE_NAME=evolution
RABBITMQ_FRAME_MAX=8192
# Global events - By enabling this variable, events from all instances are sent in the same event queue. # Global events - By enabling this variable, events from all instances are sent in the same event queue.
RABBITMQ_GLOBAL_ENABLED=false RABBITMQ_GLOBAL_ENABLED=false
# Prefix key to queue name
RABBITMQ_PREFIX_KEY=evolution
# Choose the events you want to send to RabbitMQ # Choose the events you want to send to RabbitMQ
RABBITMQ_EVENTS_APPLICATION_STARTUP=false RABBITMQ_EVENTS_APPLICATION_STARTUP=false
RABBITMQ_EVENTS_INSTANCE_CREATE=false RABBITMQ_EVENTS_INSTANCE_CREATE=false
@ -66,7 +60,6 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
RABBITMQ_EVENTS_MESSAGES_UPDATE=false RABBITMQ_EVENTS_MESSAGES_UPDATE=false
RABBITMQ_EVENTS_MESSAGES_DELETE=false RABBITMQ_EVENTS_MESSAGES_DELETE=false
RABBITMQ_EVENTS_SEND_MESSAGE=false RABBITMQ_EVENTS_SEND_MESSAGE=false
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
RABBITMQ_EVENTS_CONTACTS_SET=false RABBITMQ_EVENTS_CONTACTS_SET=false
RABBITMQ_EVENTS_CONTACTS_UPSERT=false RABBITMQ_EVENTS_CONTACTS_UPSERT=false
RABBITMQ_EVENTS_CONTACTS_UPDATE=false RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -113,7 +106,6 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
PUSHER_EVENTS_MESSAGES_UPDATE=true PUSHER_EVENTS_MESSAGES_UPDATE=true
PUSHER_EVENTS_MESSAGES_DELETE=true PUSHER_EVENTS_MESSAGES_DELETE=true
PUSHER_EVENTS_SEND_MESSAGE=true PUSHER_EVENTS_SEND_MESSAGE=true
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
PUSHER_EVENTS_CONTACTS_SET=true PUSHER_EVENTS_CONTACTS_SET=true
PUSHER_EVENTS_CONTACTS_UPSERT=true PUSHER_EVENTS_CONTACTS_UPSERT=true
PUSHER_EVENTS_CONTACTS_UPDATE=true PUSHER_EVENTS_CONTACTS_UPDATE=true
@ -155,7 +147,6 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
WEBHOOK_EVENTS_MESSAGES_UPDATE=true WEBHOOK_EVENTS_MESSAGES_UPDATE=true
WEBHOOK_EVENTS_MESSAGES_DELETE=true WEBHOOK_EVENTS_MESSAGES_DELETE=true
WEBHOOK_EVENTS_SEND_MESSAGE=true WEBHOOK_EVENTS_SEND_MESSAGE=true
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
WEBHOOK_EVENTS_CONTACTS_SET=true WEBHOOK_EVENTS_CONTACTS_SET=true
WEBHOOK_EVENTS_CONTACTS_UPSERT=true WEBHOOK_EVENTS_CONTACTS_UPSERT=true
WEBHOOK_EVENTS_CONTACTS_UPDATE=true WEBHOOK_EVENTS_CONTACTS_UPDATE=true
@ -180,15 +171,6 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
WEBHOOK_EVENTS_ERRORS=false WEBHOOK_EVENTS_ERRORS=false
WEBHOOK_EVENTS_ERRORS_WEBHOOK= WEBHOOK_EVENTS_ERRORS_WEBHOOK=
WEBHOOK_REQUEST_TIMEOUT_MS=60000
WEBHOOK_RETRY_MAX_ATTEMPTS=10
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
WEBHOOK_RETRY_JITTER_FACTOR=0.2
# Comma separated list of HTTP status codes that should not trigger retries
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
# Name that will be displayed on smartphone connection # Name that will be displayed on smartphone connection
CONFIG_SESSION_PHONE_CLIENT=Evolution API CONFIG_SESSION_PHONE_CLIENT=Evolution API
# Browser Name = Chrome | Firefox | Edge | Opera | Safari # Browser Name = Chrome | Firefox | Edge | Opera | Safari
@ -196,7 +178,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel # Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web # https://web.whatsapp.com/check-update?version=0&platform=web
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200 CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30
@ -226,12 +208,6 @@ OPENAI_ENABLED=false
# Dify - Environment variables # Dify - Environment variables
DIFY_ENABLED=false DIFY_ENABLED=false
# n8n - Environment variables
N8N_ENABLED=false
# EvoAI - Environment variables
EVOAI_ENABLED=false
# Cache - Environment variables # Cache - Environment variables
# Redis Cache enabled # Redis Cache enabled
CACHE_REDIS_ENABLED=true CACHE_REDIS_ENABLED=true
@ -272,10 +248,6 @@ S3_USE_SSL=true
# S3_USE_SSL=true # S3_USE_SSL=true
# S3_REGION=eu-south # S3_REGION=eu-south
# Evolution Audio Converter - Environment variables - https://github.com/EvolutionAPI/evolution-audio-converter
# API_AUDIO_CONVERTER=http://localhost:4040/process-audio
# API_AUDIO_CONVERTER_KEY=429683C4C977415CAAFCCE10F7D57E11
# Define a global apikey to access all instances. # Define a global apikey to access all instances.
# OBS: This key must be inserted in the request header to create an instance. # OBS: This key must be inserted in the request header to create an instance.
AUTHENTICATION_API_KEY=429683C4C977415CAAFCCE10F7D57E11 AUTHENTICATION_API_KEY=429683C4C977415CAAFCCE10F7D57E11
@ -288,4 +260,4 @@ LANGUAGE=en
# PROXY_PORT=80 # PROXY_PORT=80
# PROXY_PROTOCOL=http # PROXY_PROTOCOL=http
# PROXY_USERNAME= # PROXY_USERNAME=
# PROXY_PASSWORD= # PROXY_PASSWORD=

View File

@ -1,11 +1,7 @@
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
project: 'tsconfig.json', sourceType: 'CommonJS',
tsconfigRootDir: __dirname,
sourceType: 'module',
warnOnUnsupportedTypeScriptVersion: false,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
}, },
plugins: ['@typescript-eslint', 'simple-import-sort', 'import'], plugins: ['@typescript-eslint', 'simple-import-sort', 'import'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],

View File

@ -1,28 +0,0 @@
name: Check Code Quality
on: [pull_request]
jobs:
check-lint-and-build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Install packages
run: npm install
- name: Check linting
run: npm run lint:check
- name: Check build
run: npm run db:generate
- name: Check build
run: npm run build

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

@ -2,8 +2,6 @@
/dist /dist
/node_modules /node_modules
.cursor*
/Docker/.env /Docker/.env
.vscode .vscode
@ -23,6 +21,7 @@ lerna-debug.log*
# Package # Package
/yarn.lock /yarn.lock
/pnpm-lock.yaml /pnpm-lock.yaml
/package-lock.json
# IDEs # IDEs
.vscode/* .vscode/*

View File

@ -1,67 +1,4 @@
# 2.3.0 (2025-06-17 09:19) # 2.2.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.18
* Audio send duplicate from chatwoot
* Chatwoot csat creating new conversation in another language
* Refactor SQS controller to correct bug in sqs events by instance
* Adjustin cloud api send audio and video
* Preserve animation in GIF and WebP stickers
* Preventing use conversation from other inbox for the same user
* Ensure full WhatsApp compatibility for audio conversion (libopus, 48kHz, mono)
* Enhance message fetching and processing logic
* Added lid on whatsapp numbers router
* Now if the CONFIG_SESSION_PHONE_VERSION variable is not filled in it automatically searches for the most updated version
### Security
* Change execSync to execFileSync
* Enhance WebSocket authentication and connection handling
# 2.2.3 (2025-02-03 11:52)
### Fixed
* Fix cache in local file system
* Update Baileys Version
# 2.2.2 (2025-01-31 06:55)
### Features
* Added prefix key to queue name in RabbitMQ
### Fixed
* Update Baileys Version
# 2.2.1 (2025-01-22 14:37)
### Features
* Retry system for send webhooks
* Message filtering to support timestamp range queries
* Chats filtering to support timestamp range queries
### Fixed
* Correction of webhook global
* Fixed send audio with whatsapp cloud api
* Refactor on fetch chats
* Refactor on Evolution Channel
# 2.2.0 (2024-10-18 10:00)
### Features ### Features
@ -71,8 +8,6 @@
* Added unreadMessages to chats * Added unreadMessages to chats
* Pusher event integration * Pusher event integration
* Add support for splitMessages and timePerChar in Integrations * Add support for splitMessages and timePerChar in Integrations
* Audio Converter via API
* Send PTV messages with Baileys
### Fixed ### Fixed
@ -84,8 +19,6 @@
* Add indexes to improve performance in Evolution * Add indexes to improve performance in Evolution
* Add logical or permanent message deletion based on env config * Add logical or permanent message deletion based on env config
* Add support for fetching multiple instances by key * Add support for fetching multiple instances by key
* Update instance.controller.ts to filter by instanceName
* Receive template button reply message
# 2.1.2 (2024-10-06 10:09) # 2.1.2 (2024-10-06 10:09)

View File

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

View File

@ -1,17 +1,17 @@
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
RUN apk update && \ RUN apk update && \
apk add --no-cache git ffmpeg wget curl bash openssl apk add git ffmpeg wget curl bash
LABEL version="2.3.0" description="Api to control whatsapp features through http requests." LABEL version="2.2.0" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@evolution-api.com" LABEL contact="contato@atendai.com"
WORKDIR /evolution WORKDIR /evolution
COPY ./package.json ./tsconfig.json ./ COPY ./package.json ./tsconfig.json ./
RUN npm install RUN npm install -f
COPY ./src ./src COPY ./src ./src
COPY ./public ./public COPY ./public ./public
@ -32,7 +32,7 @@ RUN npm run build
FROM node:20-alpine AS final FROM node:20-alpine AS final
RUN apk update && \ RUN apk update && \
apk add tzdata ffmpeg bash openssl apk add tzdata ffmpeg bash
ENV TZ=America/Sao_Paulo ENV TZ=America/Sao_Paulo

View File

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

View File

@ -2,7 +2,6 @@
<div align="center"> <div align="center">
[![Docker Image (https://img.shields.io/badge/Docker-Image-blue)](https://hub.docker.com/r/evoapicloud/evolution-api)]
[![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](https://evolution-api.com/whatsapp) [![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](https://evolution-api.com/whatsapp)
[![Discord Community](https://img.shields.io/badge/Discord-Community-blue)](https://evolution-api.com/discord) [![Discord Community](https://img.shields.io/badge/Discord-Community-blue)](https://evolution-api.com/discord)
[![Postman Collection](https://img.shields.io/badge/Postman-Collection-orange)](https://evolution-api.com/postman) [![Postman Collection](https://img.shields.io/badge/Postman-Collection-orange)](https://evolution-api.com/postman)
@ -76,6 +75,10 @@ To continuously improve our services, we have implemented telemetry that collect
Join our Evolution Pro community for expert support and a weekly call to answer questions. Visit the link below to learn more and subscribe: Join our Evolution Pro community for expert support and a weekly call to answer questions. Visit the link below to learn more and subscribe:
[Click here to learn more](https://evolution-api.com/suporte-pro) [Click here to learn more](https://evolution-api.com/suporte-pro)
<br>
<a href="https://evolution-api.com/suporte-pro">
<img src="./public/images/evolution-pro.png" alt="Subscribe" width="600">
</a>
# Donate to the project. # Donate to the project.
@ -88,7 +91,6 @@ https://github.com/sponsors/EvolutionAPI
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API: We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
- [Promovaweb](https://www.youtube.com/@promovaweb) - [Promovaweb](https://www.youtube.com/@promovaweb)
- [Sandeco](https://www.youtube.com/@canalsandeco)
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG) - [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
- [Francis MNO](https://www.youtube.com/@FrancisMNO) - [Francis MNO](https://www.youtube.com/@FrancisMNO)
- [Pablo Cabral](https://youtube.com/@pablocabral) - [Pablo Cabral](https://youtube.com/@pablocabral)
@ -113,7 +115,7 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer. 2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
Please contact contato@evolution-api.com to inquire about licensing matters. Please contact contato@atendai.com to inquire about licensing matters.
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).

View File

@ -1,11 +1,8 @@
services: services:
api: api:
container_name: evolution_api container_name: evolution_api
image: evoapicloud/evolution-api:latest image: atendai/evolution-api:v2.0.9-rc
restart: always restart: always
depends_on:
- redis
- postgres
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
@ -17,41 +14,8 @@ services:
expose: expose:
- 8080 - 8080
redis:
image: redis:latest
networks:
- evolution-net
container_name: redis
command: >
redis-server --port 6379 --appendonly yes
volumes:
- evolution_redis:/data
ports:
- 6379:6379
postgres:
container_name: postgres
image: postgres:15
networks:
- evolution-net
command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"]
restart: always
ports:
- 5432:5432
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=evolution
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
- 5432
volumes: volumes:
evolution_instances: evolution_instances:
evolution_redis:
postgres_data:
networks: networks:

View File

@ -1,150 +0,0 @@
#!/bin/bash
# Definir cores para melhor legibilidade
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Função para log
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Verificar se está rodando como root
if [ "$(id -u)" = "0" ]; then
log_error "Este script não deve ser executado como root"
exit 1
fi
# Verificar sistema operacional
OS="$(uname -s)"
case "${OS}" in
Linux*)
if [ ! -x "$(command -v curl)" ]; then
log_warning "Curl não está instalado. Tentando instalar..."
if [ -x "$(command -v apt-get)" ]; then
sudo apt-get update && sudo apt-get install -y curl
elif [ -x "$(command -v yum)" ]; then
sudo yum install -y curl
else
log_error "Não foi possível instalar curl automaticamente. Por favor, instale manualmente."
exit 1
fi
fi
;;
Darwin*)
if [ ! -x "$(command -v curl)" ]; then
log_error "Curl não está instalado. Por favor, instale o Xcode Command Line Tools."
exit 1
fi
;;
*)
log_error "Sistema operacional não suportado: ${OS}"
exit 1
;;
esac
# Verificar conexão com a internet antes de prosseguir
if ! ping -c 1 8.8.8.8 &> /dev/null; then
log_error "Sem conexão com a internet. Por favor, verifique sua conexão."
exit 1
fi
# Adicionar verificação de espaço em disco
REQUIRED_SPACE=1000000 # 1GB em KB
AVAILABLE_SPACE=$(df -k . | awk 'NR==2 {print $4}')
if [ $AVAILABLE_SPACE -lt $REQUIRED_SPACE ]; then
log_error "Espaço em disco insuficiente. Necessário pelo menos 1GB livre."
exit 1
fi
# Adicionar tratamento de erro para comandos npm
npm_install_with_retry() {
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
log "Tentativa $attempt de $max_attempts para npm install"
if npm install; then
return 0
fi
attempt=$((attempt + 1))
[ $attempt -le $max_attempts ] && log_warning "Falha na instalação. Tentando novamente em 5 segundos..." && sleep 5
done
log_error "Falha ao executar npm install após $max_attempts tentativas"
return 1
}
# Adicionar timeout para comandos
execute_with_timeout() {
timeout 300 $@ || log_error "Comando excedeu o tempo limite de 5 minutos: $@"
}
# Verificar se o NVM já está instalado
if [ -d "$HOME/.nvm" ]; then
log "NVM já está instalado."
else
log "Instalando NVM..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
fi
# Carregar o NVM no ambiente atual
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
# Verificar se a versão do Node.js já está instalada
if command -v node >/dev/null 2>&1 && [ "$(node -v)" = "v20.10.0" ]; then
log "Node.js v20.10.0 já está instalado."
else
log "Instalando Node.js v20.10.0..."
nvm install v20.10.0
fi
nvm use v20.10.0
# Verificar as versões instaladas
log "Verificando as versões instaladas:"
log "Node.js: $(node -v)"
log "npm: $(npm -v)"
# Instala dependências do projeto
log "Instalando dependências do projeto..."
rm -rf node_modules
npm install
# Deploy do banco de dados
log "Deploy do banco de dados..."
npm run db:generate
npm run db:deploy
# Iniciar o projeto
log "Iniciando o projeto..."
if [ "$1" = "-dev" ]; then
npm run dev:server
else
npm run build
npm run start:prod
fi
log "Instalação concluída com sucesso!"
# Criar arquivo de log
LOGFILE="./installation_log_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee -a "$LOGFILE")
exec 2>&1
# Adicionar trap para limpeza em caso de interrupção
cleanup() {
log "Limpando recursos temporários..."
# Adicione comandos de limpeza aqui
}
trap cleanup EXIT

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" /> <link rel="icon" type="image/png" href="/assets/images/evolution-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Evolution Manager</title> <title>Evolution Manager</title>
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script> <script type="module" crossorigin src="/assets/index-CFAZX6IV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css"> <link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

12330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.3.0", "version": "2.2.0",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js", "main": "./dist/main.js",
"type": "commonjs", "type": "commonjs",
@ -8,16 +8,13 @@
"build": "tsc --noEmit && tsup", "build": "tsc --noEmit && tsup",
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts", "start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"dev:server": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts", "dev:server": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
"test": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts", "test": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
"lint": "eslint --fix --ext .ts src", "lint": "eslint --fix --ext .ts src",
"lint:check": "eslint --ext .ts src",
"db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
"db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "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": "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": { "repository": {
"type": "git", "type": "git",
@ -41,7 +38,7 @@
], ],
"author": { "author": {
"name": "Davidson Gomes", "name": "Davidson Gomes",
"email": "contato@evolution-api.com" "email": "contato@atendai.com"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"bugs": { "bugs": {
@ -50,78 +47,71 @@
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme", "homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "^0.2.4", "@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-sqs": "^3.723.0", "@aws-sdk/client-sqs": "^3.569.0",
"@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffmpeg-installer/ffmpeg": "^1.1.0",
"@figuro/chatwoot-sdk": "^1.1.16", "@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.15.0",
"@prisma/client": "^6.1.0", "@sentry/node": "^8.28.0",
"@sentry/node": "^8.47.0", "amqplib": "^0.10.3",
"amqplib": "^0.10.5", "axios": "^1.6.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "github:EvolutionAPI/Baileys", "baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"compression": "^1.7.5", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"dayjs": "^1.11.13", "dayjs": "^1.11.7",
"dotenv": "^16.4.7", "dotenv": "^16.4.5",
"eventemitter2": "^6.4.9", "eventemitter2": "^6.4.9",
"express": "^4.21.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.2",
"form-data": "^4.0.1", "form-data": "^4.0.0",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.2",
"i18next": "^23.7.19", "i18next": "^23.7.19",
"jimp": "^0.16.13", "jimp": "^0.16.13",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2", "link-preview-js": "^3.0.4",
"link-preview-js": "^3.0.13",
"long": "^5.2.3", "long": "^5.2.3",
"mediainfo.js": "^0.3.4", "mime": "^3.0.0",
"mime": "^4.0.0", "minio": "^8.0.1",
"mime-types": "^2.1.35",
"minio": "^8.0.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nats": "^2.29.1",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"openai": "^4.77.3", "openai": "^4.52.7",
"pg": "^8.13.1", "pg": "^8.11.3",
"pino": "^8.11.0", "pino": "^8.11.0",
"prisma": "^6.1.0", "prisma": "^5.15.0",
"pusher": "^5.2.0", "pusher": "^5.2.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"redis": "^4.7.0", "redis": "^4.6.5",
"sharp": "^0.32.6", "sharp": "^0.32.2",
"socket.io": "^4.8.1", "socket.io": "^4.7.1",
"socket.io-client": "^4.8.1", "tsup": "^8.2.4",
"tsup": "^8.3.5" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.5", "@types/compression": "^1.7.2",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.13",
"@types/express": "^4.17.18", "@types/express": "^4.17.17",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"@types/mime": "^4.0.0", "@types/mime": "3.0.0",
"@types/mime-types": "^2.1.4", "@types/node": "^18.15.11",
"@types/node": "^22.10.5",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.0",
"@types/qrcode-terminal": "^0.12.2", "@types/qrcode-terminal": "^0.12.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^3.4.2", "prettier": "^2.8.8",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.2" "typescript": "^5.5.4"
} }
} }

View File

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

View File

@ -1,232 +0,0 @@
/*
Warnings:
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `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`.
*/
-- AlterTable
ALTER TABLE `Chat` ADD COLUMN `unreadMessages` INTEGER NOT NULL DEFAULT 0,
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` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `DifySetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBotSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Flowise` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `FlowiseSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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 `Message` MODIFY `status` VARCHAR(30) NULL;
-- AlterTable
ALTER TABLE `OpenaiBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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 `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` 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;
-- CreateTable
CREATE TABLE `Pusher` (
`id` VARCHAR(191) NOT NULL,
`enabled` BOOLEAN NOT NULL DEFAULT false,
`appId` VARCHAR(100) NOT NULL,
`key` VARCHAR(100) NOT NULL,
`secret` VARCHAR(100) NOT NULL,
`cluster` VARCHAR(100) NOT NULL,
`useTLS` BOOLEAN NOT NULL DEFAULT false,
`events` JSON NOT NULL,
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP NOT NULL,
`instanceId` VARCHAR(191) NOT NULL,
UNIQUE INDEX `Pusher_instanceId_key`(`instanceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateIndex
CREATE INDEX `Chat_remoteJid_idx` ON `Chat`(`remoteJid`);
-- CreateIndex
CREATE INDEX `Contact_remoteJid_idx` ON `Contact`(`remoteJid`);
-- CreateIndex
CREATE INDEX `Setting_instanceId_idx` ON `Setting`(`instanceId`);
-- CreateIndex
CREATE INDEX `Webhook_instanceId_idx` ON `Webhook`(`instanceId`);
-- AddForeignKey
ALTER TABLE `Pusher` ADD CONSTRAINT `Pusher_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- RenameIndex
ALTER TABLE `Chat` RENAME INDEX `Chat_instanceId_fkey` TO `Chat_instanceId_idx`;
-- RenameIndex
ALTER TABLE `Contact` RENAME INDEX `Contact_instanceId_fkey` TO `Contact_instanceId_idx`;
-- RenameIndex
ALTER TABLE `Message` RENAME INDEX `Message_instanceId_fkey` TO `Message_instanceId_idx`;
-- RenameIndex
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_instanceId_fkey` TO `MessageUpdate_instanceId_idx`;
-- RenameIndex
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_messageId_fkey` TO `MessageUpdate_messageId_idx`;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Setting'
AND column_name = 'wavoipToken'
) THEN
ALTER TABLE "Setting" ADD COLUMN "wavoipToken" VARCHAR(100);
END IF;
END $$;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,31 +1,19 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { existsSync } = require('fs');
dotenv.config(); dotenv.config();
const { DATABASE_PROVIDER } = process.env; const { DATABASE_PROVIDER } = process.env;
const databaseProviderDefault = DATABASE_PROVIDER ?? 'postgresql'; const databaseProviderDefault = DATABASE_PROVIDER ?? "postgresql"
if (!DATABASE_PROVIDER) { if (!DATABASE_PROVIDER) {
console.warn(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`); console.error(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
// process.exit(1);
} }
let command = process.argv const command = process.argv
.slice(2) .slice(2)
.join(' ') .join(' ')
.replace(/DATABASE_PROVIDER/g, databaseProviderDefault); .replace(/\DATABASE_PROVIDER/g, databaseProviderDefault);
if (command.includes('rmdir') && existsSync('prisma\\migrations')) {
try {
execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' });
} catch (error) {
console.error(`Error removing directory: prisma\\migrations`);
process.exit(1);
}
} else if (command.includes('rmdir')) {
console.warn(`Directory 'prisma\\migrations' does not exist, skipping removal.`);
}
try { try {
execSync(command, { stdio: 'inherit' }); execSync(command, { stdio: 'inherit' });

View File

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

View File

@ -63,9 +63,6 @@ export class InstanceController {
instanceId, instanceId,
integration: instanceData.integration, integration: instanceData.integration,
instanceName: instanceData.instanceName, instanceName: instanceData.instanceName,
ownerJid: instanceData.ownerJid,
profileName: instanceData.profileName,
profilePicUrl: instanceData.profilePicUrl,
hash, hash,
number: instanceData.number, number: instanceData.number,
businessId: instanceData.businessId, businessId: instanceData.businessId,
@ -122,7 +119,6 @@ export class InstanceController {
readMessages: instanceData.readMessages === true, readMessages: instanceData.readMessages === true,
readStatus: instanceData.readStatus === true, readStatus: instanceData.readStatus === true,
syncFullHistory: instanceData.syncFullHistory === true, syncFullHistory: instanceData.syncFullHistory === true,
wavoipToken: instanceData.wavoipToken || '',
}; };
await this.settingsService.create(instance, settings); await this.settingsService.create(instance, settings);
@ -170,9 +166,6 @@ export class InstanceController {
rabbitmq: { rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled, enabled: instanceData?.rabbitmq?.enabled,
}, },
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: { sqs: {
enabled: instanceData?.sqs?.enabled, enabled: instanceData?.sqs?.enabled,
}, },
@ -261,9 +254,6 @@ export class InstanceController {
rabbitmq: { rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled, enabled: instanceData?.rabbitmq?.enabled,
}, },
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: { sqs: {
enabled: instanceData?.sqs?.enabled, enabled: instanceData?.sqs?.enabled,
}, },
@ -392,9 +382,7 @@ export class InstanceController {
return this.waMonitor.instanceInfoById(instanceId, number); return this.waMonitor.instanceInfoById(instanceId, number);
} }
const instanceNames = instanceName ? [instanceName] : null; return this.waMonitor.instanceInfo();
return this.waMonitor.instanceInfo(instanceNames);
} }
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) { public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
@ -419,11 +407,15 @@ export class InstanceController {
public async deleteInstance({ instanceName }: InstanceDto) { public async deleteInstance({ instanceName }: InstanceDto) {
const { instance } = await this.connectionState({ instanceName }); const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'open') {
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
}
try { try {
const waInstances = this.waMonitor.waInstances[instanceName]; const waInstances = this.waMonitor.waInstances[instanceName];
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot(); if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
if (instance.state === 'connecting' || instance.state === 'open') { if (instance.state === 'connecting') {
await this.logout({ instanceName }); await this.logout({ instanceName });
} }

View File

@ -10,10 +10,7 @@ import axios from 'axios';
const logger = new Logger('ProxyController'); const logger = new Logger('ProxyController');
export class ProxyController { export class ProxyController {
constructor( constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
private readonly proxyService: ProxyService,
private readonly waMonitor: WAMonitoringService,
) {}
public async createProxy(instance: InstanceDto, data: ProxyDto) { public async createProxy(instance: InstanceDto, data: ProxyDto) {
if (!this.waMonitor.waInstances[instance.instanceName]) { if (!this.waMonitor.waInstances[instance.instanceName]) {

View File

@ -7,7 +7,6 @@ import {
SendLocationDto, SendLocationDto,
SendMediaDto, SendMediaDto,
SendPollDto, SendPollDto,
SendPtvDto,
SendReactionDto, SendReactionDto,
SendStatusDto, SendStatusDto,
SendStickerDto, SendStickerDto,
@ -18,14 +17,6 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { isBase64, isURL } from 'class-validator'; import { isBase64, isURL } from 'class-validator';
function isEmoji(str: string) {
if (str === '') return true;
const emojiRegex =
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
return emojiRegex.test(str);
}
export class SendMessageController { export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {} constructor(private readonly waMonitor: WAMonitoringService) {}
@ -48,13 +39,6 @@ export class SendMessageController {
throw new BadRequestException('Owned media must be a url or base64'); throw new BadRequestException('Owned media must be a url or base64');
} }
public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) {
if (file || isURL(data?.video) || isBase64(data?.video)) {
return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) { public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) {
if (file || isURL(data.sticker) || isBase64(data.sticker)) { if (file || isURL(data.sticker) || isBase64(data.sticker)) {
return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file); return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file);
@ -89,8 +73,8 @@ export class SendMessageController {
} }
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!isEmoji(data.reaction)) { if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
throw new BadRequestException('Reaction must be a single emoji or empty string'); throw new BadRequestException('"reaction" must be an emoji');
} }
return await this.waMonitor.waInstances[instanceName].reactionMessage(data); return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
} }

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { IntegrationDto } from '@api/integrations/integration.dto'; import { IntegrationDto } from '@api/integrations/integration.dto';
import { JsonValue } from '@prisma/client/runtime/library';
import { WAPresence } from 'baileys'; import { WAPresence } from 'baileys';
export class InstanceDto extends IntegrationDto { export class InstanceDto extends IntegrationDto {
@ -11,9 +10,6 @@ export class InstanceDto extends IntegrationDto {
integration?: string; integration?: string;
token?: string; token?: string;
status?: string; status?: string;
ownerJid?: string;
profileName?: string;
profilePicUrl?: string;
// settings // settings
rejectCall?: boolean; rejectCall?: boolean;
msgCall?: string; msgCall?: string;
@ -22,35 +18,12 @@ export class InstanceDto extends IntegrationDto {
readMessages?: boolean; readMessages?: boolean;
readStatus?: boolean; readStatus?: boolean;
syncFullHistory?: boolean; syncFullHistory?: boolean;
wavoipToken?: string;
// proxy // proxy
proxyHost?: string; proxyHost?: string;
proxyPort?: string; proxyPort?: string;
proxyProtocol?: string; proxyProtocol?: string;
proxyUsername?: string; proxyUsername?: string;
proxyPassword?: string; proxyPassword?: string;
webhook?: {
enabled?: boolean;
events?: string[];
headers?: JsonValue;
url?: string;
byEvents?: boolean;
base64?: boolean;
};
chatwootAccountId?: string;
chatwootConversationPending?: boolean;
chatwootAutoCreate?: boolean;
chatwootDaysLimitImportMessages?: number;
chatwootImportContacts?: boolean;
chatwootImportMessages?: boolean;
chatwootLogo?: string;
chatwootMergeBrazilContacts?: boolean;
chatwootNameInbox?: string;
chatwootOrganization?: string;
chatwootReopenConversation?: boolean;
chatwootSignMsg?: boolean;
chatwootToken?: string;
chatwootUrl?: string;
} }
export class SetPresenceDto { export class SetPresenceDto {

View File

@ -44,7 +44,6 @@ export class Metadata {
mentionsEveryOne?: boolean; mentionsEveryOne?: boolean;
mentioned?: string[]; mentioned?: string[];
encoding?: boolean; encoding?: boolean;
notConvertSticker?: boolean;
} }
export class SendTextDto extends Metadata { export class SendTextDto extends Metadata {
@ -71,7 +70,7 @@ export class SendPollDto extends Metadata {
messageSecret?: Uint8Array; messageSecret?: Uint8Array;
} }
export type MediaType = 'image' | 'document' | 'video' | 'audio' | 'ptv'; export type MediaType = 'image' | 'document' | 'video' | 'audio';
export class SendMediaDto extends Metadata { export class SendMediaDto extends Metadata {
mediatype: MediaType; mediatype: MediaType;
@ -83,10 +82,6 @@ export class SendMediaDto extends Metadata {
media: string; media: string;
} }
export class SendPtvDto extends Metadata {
video: string;
}
export class SendStickerDto extends Metadata { export class SendStickerDto extends Metadata {
sticker: string; sticker: string;
} }
@ -95,21 +90,15 @@ export class SendAudioDto extends Metadata {
audio: string; audio: string;
} }
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix'; export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
export class Button { export class Button {
type: TypeButton; type: TypeButton;
displayText?: string; displayText: string;
id?: string; id?: string;
url?: string; url?: string;
copyCode?: string; copyCode?: string;
phoneNumber?: string; phoneNumber?: string;
currency?: string;
name?: string;
keyType?: KeyType;
key?: string;
} }
export class SendButtonsDto extends Metadata { export class SendButtonsDto extends Metadata {

View File

@ -6,5 +6,4 @@ export class SettingsDto {
readMessages?: boolean; readMessages?: boolean;
readStatus?: boolean; readStatus?: boolean;
syncFullHistory?: boolean; syncFullHistory?: boolean;
wavoipToken?: string;
} }

View File

@ -75,6 +75,8 @@ export class ChannelController {
data.prismaRepository, data.prismaRepository,
data.cache, data.cache,
data.chatwootCache, data.chatwootCache,
data.baileysCache,
data.providerFiles,
); );
} }

View File

@ -2,16 +2,14 @@ import { Router } from 'express';
import { EvolutionRouter } from './evolution/evolution.router'; import { EvolutionRouter } from './evolution/evolution.router';
import { MetaRouter } from './meta/meta.router'; import { MetaRouter } from './meta/meta.router';
import { BaileysRouter } from './whatsapp/baileys.router';
export class ChannelRouter { export class ChannelRouter {
public readonly router: Router; public readonly router: Router;
constructor(configService: any, ...guards: any[]) { constructor(configService: any) {
this.router = Router(); this.router = Router();
this.router.use('/', new EvolutionRouter(configService).router); this.router.use('/', new EvolutionRouter(configService).router);
this.router.use('/', new MetaRouter(configService).router); this.router.use('/', new MetaRouter(configService).router);
this.router.use('/baileys', new BaileysRouter(...guards).router);
} }
} }

View File

@ -1,27 +1,16 @@
import { InstanceDto } from '@api/dto/instance.dto'; import { MediaMessage, Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
import { import { ProviderFiles } from '@api/provider/sessions';
MediaMessage,
Options,
SendAudioDto,
SendButtonsDto,
SendMediaDto,
SendTextDto,
} from '@api/dto/sendMessage.dto';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { chatbotController } from '@api/server.module'; import { chatbotController } from '@api/server.module';
import { CacheService } from '@api/services/cache.service'; import { CacheService } from '@api/services/cache.service';
import { ChannelStartupService } from '@api/services/channel.service'; import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types'; import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Openai, S3 } from '@config/env.config'; import { Chatwoot, ConfigService, Openai } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid'; import { status } from '@utils/renderStatus';
import axios from 'axios'; import { isURL } from 'class-validator';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data'; import mime from 'mime';
import mimeTypes from 'mime-types';
import { join } from 'path';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
export class EvolutionStartupService extends ChannelStartupService { export class EvolutionStartupService extends ChannelStartupService {
@ -31,6 +20,8 @@ export class EvolutionStartupService extends ChannelStartupService {
public readonly prismaRepository: PrismaRepository, public readonly prismaRepository: PrismaRepository,
public readonly cache: CacheService, public readonly cache: CacheService,
public readonly chatwootCache: CacheService, public readonly chatwootCache: CacheService,
public readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) { ) {
super(configService, eventEmitter, prismaRepository, chatwootCache); super(configService, eventEmitter, prismaRepository, chatwootCache);
@ -65,34 +56,8 @@ export class EvolutionStartupService extends ChannelStartupService {
await this.closeClient(); await this.closeClient();
} }
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceId);
this.instance.name = instance.instanceName;
this.instance.id = instance.instanceId;
this.instance.integration = instance.integration;
this.instance.number = instance.number;
this.instance.token = instance.token;
this.instance.businessId = instance.businessId;
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
Events.STATUS_INSTANCE,
{
instanceName: this.instance.name,
instanceId: this.instance.id,
integration: instance.integration,
},
{
instance: this.instance.name,
status: 'created',
},
);
}
}
public async profilePicture(number: string) { public async profilePicture(number: string) {
const jid = createJid(number); const jid = this.createJid(number);
return { return {
wuid: jid, wuid: jid,
@ -113,12 +78,11 @@ export class EvolutionStartupService extends ChannelStartupService {
} }
public async connectToWhatsapp(data?: any): Promise<any> { public async connectToWhatsapp(data?: any): Promise<any> {
if (!data) { if (!data) return;
this.loadChatwoot();
return;
}
try { try {
this.loadChatwoot();
this.eventHandler(data); this.eventHandler(data);
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -135,7 +99,6 @@ export class EvolutionStartupService extends ChannelStartupService {
id: received.key.id || v4(), id: received.key.id || v4(),
remoteJid: received.key.remoteJid, remoteJid: received.key.remoteJid,
fromMe: received.key.fromMe, fromMe: received.key.fromMe,
profilePicUrl: received.profilePicUrl,
}; };
messageRaw = { messageRaw = {
key, key,
@ -147,9 +110,7 @@ export class EvolutionStartupService extends ChannelStartupService {
instanceId: this.instanceId, instanceId: this.instanceId,
}; };
const isAudio = received?.message?.audioMessage; if (this.configService.get<Openai>('OPENAI').ENABLED) {
if (this.configService.get<Openai>('OPENAI').ENABLED && isAudio) {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: { where: {
instanceId: this.instanceId, instanceId: this.instanceId,
@ -165,7 +126,11 @@ export class EvolutionStartupService extends ChannelStartupService {
openAiDefaultSettings.speechToText && openAiDefaultSettings.speechToText &&
received?.message?.audioMessage received?.message?.audioMessage
) { ) {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
received,
this.client.updateMediaMessage,
);
} }
} }
@ -200,7 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService {
await this.updateContact({ await this.updateContact({
remoteJid: messageRaw.key.remoteJid, remoteJid: messageRaw.key.remoteJid,
pushName: messageRaw.pushName, pushName: messageRaw.key.fromMe ? '' : messageRaw.key.fromMe == null ? '' : received.pushName,
profilePicUrl: received.profilePicUrl, profilePicUrl: received.profilePicUrl,
}); });
} }
@ -210,6 +175,35 @@ export class EvolutionStartupService extends ChannelStartupService {
} }
private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) { private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) {
const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
});
if (contact) {
const contactRaw: any = {
remoteJid: data.remoteJid,
pushName: data?.pushName,
instanceId: this.instanceId,
profilePicUrl: data?.profilePicUrl,
};
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
await this.chatwootService.eventWhatsapp(
Events.CONTACTS_UPDATE,
{ instanceName: this.instance.name, instanceId: this.instanceId },
contactRaw,
);
}
await this.prismaRepository.contact.updateMany({
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
data: contactRaw,
});
return;
}
const contactRaw: any = { const contactRaw: any = {
remoteJid: data.remoteJid, remoteJid: data.remoteJid,
pushName: data?.pushName, pushName: data?.pushName,
@ -217,40 +211,11 @@ export class EvolutionStartupService extends ChannelStartupService {
profilePicUrl: data?.profilePicUrl, profilePicUrl: data?.profilePicUrl,
}; };
const existingContact = await this.prismaRepository.contact.findFirst({
where: {
remoteJid: data.remoteJid,
instanceId: this.instanceId,
},
});
if (existingContact) {
await this.prismaRepository.contact.updateMany({
where: {
remoteJid: data.remoteJid,
instanceId: this.instanceId,
},
data: contactRaw,
});
} else {
await this.prismaRepository.contact.create({
data: contactRaw,
});
}
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { await this.prismaRepository.contact.create({
await this.chatwootService.eventWhatsapp( data: contactRaw,
Events.CONTACTS_UPDATE, });
{
instanceName: this.instance.name,
instanceId: this.instanceId,
integration: this.instance.integration,
},
contactRaw,
);
}
const chat = await this.prismaRepository.chat.findFirst({ const chat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: data.remoteJid }, where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
@ -282,13 +247,7 @@ export class EvolutionStartupService extends ChannelStartupService {
}); });
} }
protected async sendMessageWithTyping( protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
number: string,
message: any,
options?: Options,
file?: any,
isIntegration = false,
) {
try { try {
let quoted: any; let quoted: any;
let webhookUrl: any; let webhookUrl: any;
@ -313,187 +272,64 @@ export class EvolutionStartupService extends ChannelStartupService {
webhookUrl = options.webhookUrl; webhookUrl = options.webhookUrl;
} }
let audioFile;
const messageId = v4(); const messageId = v4();
let messageRaw: any; let messageRaw: any = {
key: { fromMe: true, id: messageId, remoteJid: number },
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
status: status[1],
};
if (message?.mediaType === 'image') { if (message?.mediaType === 'image') {
messageRaw = { messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number }, ...messageRaw,
message: { message: {
base64: isBase64(message.media) ? message.media : undefined, mediaUrl: message.media,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted, quoted,
}, },
messageType: 'imageMessage', messageType: 'imageMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
}; };
} else if (message?.mediaType === 'video') { } else if (message?.mediaType === 'video') {
messageRaw = { messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number }, ...messageRaw,
message: { message: {
base64: isBase64(message.media) ? message.media : undefined, mediaUrl: message.media,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted, quoted,
}, },
messageType: 'videoMessage', messageType: 'videoMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
}; };
} else if (message?.mediaType === 'audio') { } else if (message?.mediaType === 'audio') {
messageRaw = { messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number }, ...messageRaw,
message: { message: {
base64: isBase64(message.media) ? message.media : undefined, mediaUrl: message.media,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted, quoted,
}, },
messageType: 'audioMessage', messageType: 'audioMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
const buffer = Buffer.from(message.media, 'base64');
audioFile = {
buffer,
mimetype: 'audio/mp4',
originalname: `${messageId}.mp4`,
}; };
} else if (message?.mediaType === 'document') { } else if (message?.mediaType === 'document') {
messageRaw = { messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number }, ...messageRaw,
message: { message: {
base64: isBase64(message.media) ? message.media : undefined, mediaUrl: message.media,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted, quoted,
}, },
messageType: 'documentMessage', messageType: 'documentMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message.buttonMessage) {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
...message.buttonMessage,
buttons: message.buttonMessage.buttons,
footer: message.buttonMessage.footer,
body: message.buttonMessage.body,
quoted,
},
messageType: 'buttonMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message.listMessage) {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
...message.listMessage,
quoted,
},
messageType: 'listMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
}; };
} else { } else {
messageRaw = { messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number }, ...messageRaw,
message: { message: {
...message, ...message,
quoted, quoted,
}, },
messageType: 'conversation', messageType: 'conversation',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
}; };
} }
if (messageRaw.message.contextInfo) {
messageRaw.contextInfo = {
...messageRaw.message.contextInfo,
};
}
if (messageRaw.contextInfo?.stanzaId) {
const key: any = {
id: messageRaw.contextInfo.stanzaId,
};
const findMessage = await this.prismaRepository.message.findFirst({
where: {
instanceId: this.instanceId,
key,
},
});
if (findMessage) {
messageRaw.contextInfo.quotedMessage = findMessage.message;
}
}
const base64 = messageRaw.message.base64;
delete messageRaw.message.base64;
if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
}
}
this.logger.log(messageRaw); this.logger.log(messageRaw);
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
@ -539,7 +375,6 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne, mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned, mentioned: data?.mentioned,
}, },
null,
isIntegration, isIntegration,
); );
return res; return res;
@ -561,7 +396,7 @@ export class EvolutionStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4'; mediaMessage.fileName = 'video.mp4';
} }
let mimetype: string | false; let mimetype: string;
const prepareMedia: any = { const prepareMedia: any = {
caption: mediaMessage?.caption, caption: mediaMessage?.caption,
@ -572,9 +407,9 @@ export class EvolutionStartupService extends ChannelStartupService {
}; };
if (isURL(mediaMessage.media)) { if (isURL(mediaMessage.media)) {
mimetype = mimeTypes.lookup(mediaMessage.media); mimetype = mime.getType(mediaMessage.media);
} else { } else {
mimetype = mimeTypes.lookup(mediaMessage.fileName); mimetype = mime.getType(mediaMessage.fileName);
} }
prepareMedia.mimetype = mimetype; prepareMedia.mimetype = mimetype;
@ -604,78 +439,33 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne, mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned, mentioned: data?.mentioned,
}, },
file,
isIntegration, isIntegration,
); );
return mediaSent; return mediaSent;
} }
public async processAudio(audio: string, number: string, file: any) { public async processAudio(audio: string, number: string) {
number = number.replace(/\D/g, ''); number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`; const hash = `${number}-${new Date().getTime()}`;
if (process.env.API_AUDIO_CONVERTER) { let mimetype: string;
try {
this.logger.verbose('Using audio converter API');
const formData = new FormData();
if (file) { const prepareMedia: any = {
formData.append('file', file.buffer, { fileName: `${hash}.mp4`,
filename: file.originalname, mediaType: 'audio',
contentType: file.mimetype, media: audio,
}); };
} else if (isURL(audio)) {
formData.append('url', audio);
} else {
formData.append('base64', audio);
}
formData.append('format', 'mp4'); if (isURL(audio)) {
mimetype = mime.getType(audio);
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
headers: {
...formData.getHeaders(),
apikey: process.env.API_AUDIO_CONVERTER_KEY,
},
});
if (!response?.data?.audio) {
throw new InternalServerErrorException('Failed to convert audio');
}
const prepareMedia: any = {
fileName: `${hash}.mp4`,
mediaType: 'audio',
media: response?.data?.audio,
mimetype: 'audio/mpeg',
};
return prepareMedia;
} catch (error) {
this.logger.error(error?.response?.data || error);
throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error);
}
} else { } else {
let mimetype: string; mimetype = mime.getType(prepareMedia.fileName);
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
mimetype: 'audio/mpeg',
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio).toString();
} else {
mimetype = mimeTypes.lookup(prepareMedia.fileName).toString();
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
} }
prepareMedia.mimetype = mimetype;
return prepareMedia;
} }
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
@ -688,7 +478,7 @@ export class EvolutionStartupService extends ChannelStartupService {
throw new Error('File or buffer is undefined.'); throw new Error('File or buffer is undefined.');
} }
const message = await this.processAudio(mediaData.audio, data.number, file); const message = await this.processAudio(mediaData.audio, data.number);
const audioSent = await this.sendMessageWithTyping( const audioSent = await this.sendMessageWithTyping(
data.number, data.number,
@ -701,34 +491,14 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne, mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned, mentioned: data?.mentioned,
}, },
file,
isIntegration, isIntegration,
); );
return audioSent; return audioSent;
} }
public async buttonMessage(data: SendButtonsDto, isIntegration = false) { public async buttonMessage() {
return await this.sendMessageWithTyping( throw new BadRequestException('Method not available on Evolution Channel');
data.number,
{
buttonMessage: {
title: data.title,
description: data.description,
footer: data.footer,
buttons: data.buttons,
},
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
null,
isIntegration,
);
} }
public async locationMessage() { public async locationMessage() {
throw new BadRequestException('Method not available on Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel');

View File

@ -1,5 +1,6 @@
import { NumberBusiness } from '@api/dto/chat.dto'; import { NumberBusiness } from '@api/dto/chat.dto';
import { import {
Button,
ContactMessage, ContactMessage,
MediaMessage, MediaMessage,
Options, Options,
@ -12,6 +13,7 @@ import {
SendReactionDto, SendReactionDto,
SendTemplateDto, SendTemplateDto,
SendTextDto, SendTextDto,
TypeButton,
} from '@api/dto/sendMessage.dto'; } from '@api/dto/sendMessage.dto';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server'; import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { ProviderFiles } from '@api/provider/sessions'; import { ProviderFiles } from '@api/provider/sessions';
@ -22,14 +24,16 @@ import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types'; import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config'; import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid';
import { status } from '@utils/renderStatus'; import { status } from '@utils/renderStatus';
import axios from 'axios'; import axios from 'axios';
import { proto } from 'baileys';
import { arrayUnique, isURL } from 'class-validator'; import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data'; import FormData from 'form-data';
import mimeTypes from 'mime-types'; import { createReadStream } from 'fs';
import mime from 'mime';
import { join } from 'path'; import { join } from 'path';
import { v4 } from 'uuid';
export class BusinessStartupService extends ChannelStartupService { export class BusinessStartupService extends ChannelStartupService {
constructor( constructor(
@ -70,10 +74,6 @@ export class BusinessStartupService extends ChannelStartupService {
await this.closeClient(); await this.closeClient();
} }
private isMediaMessage(message: any) {
return message.document || message.image || message.audio || message.video;
}
private async post(message: any, params: string) { private async post(message: any, params: string) {
try { try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL; let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
@ -88,7 +88,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
public async profilePicture(number: string) { public async profilePicture(number: string) {
const jid = createJid(number); const jid = this.createJid(number);
return { return {
wuid: jid, wuid: jid,
@ -132,7 +132,9 @@ export class BusinessStartupService extends ChannelStartupService {
this.eventHandler(content); this.eventHandler(content);
this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id); this.phoneNumber = this.createJid(
content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id,
);
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
throw new InternalServerErrorException(error?.toString()); throw new InternalServerErrorException(error?.toString());
@ -146,20 +148,11 @@ export class BusinessStartupService extends ChannelStartupService {
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION; const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`; urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
// Primeiro, obtenha a URL do arquivo
let result = await axios.get(urlServer, { headers }); let result = await axios.get(urlServer, { headers });
result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
// Depois, baixe o arquivo usando a URL retornada
result = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
return result.data; return result.data;
} catch (e) { } catch (e) {
this.logger.error(`Error downloading media: ${e}`); this.logger.error(e);
throw e;
} }
} }
@ -167,23 +160,7 @@ export class BusinessStartupService extends ChannelStartupService {
const message = received.messages[0]; const message = received.messages[0];
let content: any = message.type + 'Message'; let content: any = message.type + 'Message';
content = { [content]: message[message.type] }; content = { [content]: message[message.type] };
if (message.context) { message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
private messageAudioJson(received: any) {
const message = received.messages[0];
let content: any = {
audioMessage: {
...message.audio,
ptt: message.audio.voice || false, // Define se é mensagem de voz
},
};
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content; return content;
} }
@ -216,77 +193,17 @@ export class BusinessStartupService extends ChannelStartupService {
} }
private messageTextJson(received: any) { private messageTextJson(received: any) {
// Verificar que received y received.messages existen
if (!received || !received.messages || received.messages.length === 0) {
this.logger.error('Error: received object or messages array is undefined or empty');
return null;
}
const message = received.messages[0];
let content: any; let content: any;
const message = received.messages[0];
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
if (!message.text) {
// Si no hay texto, manejamos diferente según el tipo de mensaje
if (message.type === 'sticker') {
content = { stickerMessage: {} };
} else if (message.type === 'location') {
content = {
locationMessage: {
degreesLatitude: message.location?.latitude,
degreesLongitude: message.location?.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
} else {
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
content = { [message.type + 'Message']: message[message.type] || {} };
}
// Añadir contexto si existe
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
// Si el mensaje tiene texto, procesamos normalmente
if (!received.metadata || !received.metadata.phone_number_id) {
this.logger.error('Error: metadata or phone_number_id is undefined');
return null;
}
if (message.from === received.metadata.phone_number_id) { if (message.from === received.metadata.phone_number_id) {
content = { content = {
extendedTextMessage: { text: message.text.body }, extendedTextMessage: { text: message.text.body },
}; };
if (message.context) { message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
} else { } else {
content = { conversation: message.text.body }; content = { conversation: message.text.body };
if (message.context) { message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
} }
return content;
}
private messageLocationJson(received: any) {
const message = received.messages[0];
let content: any = {
locationMessage: {
degreesLatitude: message.location.latitude,
degreesLongitude: message.location.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
return content; return content;
} }
@ -314,7 +231,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
if (!contact.phones[0]?.wa_id) { if (!contact.phones[0]?.wa_id) {
contact.phones[0].wa_id = createJid(contact.phones[0].phone); contact.phones[0].wa_id = this.createJid(contact.phones[0].phone);
} }
result += result +=
@ -367,12 +284,6 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template': case 'template':
messageType = 'conversation'; messageType = 'conversation';
break; break;
case 'location':
messageType = 'locationMessage';
break;
case 'sticker':
messageType = 'stickerMessage';
break;
default: default:
messageType = 'conversation'; messageType = 'conversation';
break; break;
@ -389,36 +300,22 @@ export class BusinessStartupService extends ChannelStartupService {
if (received.contacts) pushName = received.contacts[0].profile.name; if (received.contacts) pushName = received.contacts[0].profile.name;
if (received.messages) { if (received.messages) {
const message = received.messages[0]; // Añadir esta línea para definir message
const key = { const key = {
id: message.id, id: received.messages[0].id,
remoteJid: this.phoneNumber, remoteJid: this.phoneNumber,
fromMe: message.from === received.metadata.phone_number_id, fromMe: received.messages[0].from === received.metadata.phone_number_id,
}; };
if (
if (message.type === 'sticker') { received?.messages[0].document ||
this.logger.log('Procesando mensaje de tipo sticker'); received?.messages[0].image ||
received?.messages[0].audio ||
received?.messages[0].video
) {
messageRaw = { messageRaw = {
key, key,
pushName, pushName,
message: { message: this.messageMediaJson(received),
stickerMessage: message.sticker || {}, contextInfo: this.messageMediaJson(received)?.contextInfo,
},
messageType: 'stickerMessage',
messageTimestamp: parseInt(message.timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (this.isMediaMessage(message)) {
const messageContent =
message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
messageRaw = {
key,
pushName,
message: messageContent,
contextInfo: messageContent?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type), messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number, messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown', source: 'unknown',
@ -436,24 +333,17 @@ export class BusinessStartupService extends ChannelStartupService {
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers }); const result = await axios.get(urlServer, { headers });
const buffer = await axios.get(result.data.url, { const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
let mediaType; const mediaType = message.messages[0].document
? 'document'
: message.messages[0].image
? 'image'
: message.messages[0].audio
? 'audio'
: 'video';
if (message.messages[0].document) { const mimetype = result.headers['content-type'];
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
} else {
mediaType = 'video';
}
const mimetype = result.data?.mime_type || result.headers['content-type'];
const contentDisposition = result.headers['content-disposition']; const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`; let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
@ -464,32 +354,17 @@ export class BusinessStartupService extends ChannelStartupService {
} }
} }
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
}
}
const size = result.headers['content-length'] || buffer.data.byteLength; const size = result.headers['content-length'] || buffer.data.byteLength;
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName); const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer.data, size, { await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype, 'Content-Type': mimetype,
}); });
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
await this.prismaRepository.media.create({ await this.prismaRepository.media.create({
data: { data: {
messageId: createdMessage.id, messageId: received.messages[0].id,
instanceId: this.instanceId, instanceId: this.instanceId,
type: mediaType, type: mediaType,
fileName: fullName, fileName: fullName,
@ -500,73 +375,13 @@ export class BusinessStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName); const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
} else { } else {
const buffer = await this.downloadMediaMessage(received?.messages[0]); const buffer = await this.downloadMediaMessage(received?.messages[0]);
messageRaw.message.base64 = buffer.toString('base64'); messageRaw.message.base64 = buffer.toString('base64');
// Processar OpenAI speech-to-text para áudio mesmo sem S3
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
base64: messageRaw.message.base64,
...messageRaw,
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
} }
} else if (received?.messages[0].interactive) { } else if (received?.messages[0].interactive) {
messageRaw = { messageRaw = {
@ -637,6 +452,30 @@ export class BusinessStartupService extends ChannelStartupService {
// await this.client.readMessages([received.key]); // await this.client.readMessages([received.key]);
} }
if (this.configService.get<Openai>('OPENAI').ENABLED) {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText &&
received?.message?.audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
received,
this.client.updateMediaMessage,
);
}
}
this.logger.log(messageRaw); this.logger.log(messageRaw);
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
@ -662,11 +501,9 @@ export class BusinessStartupService extends ChannelStartupService {
} }
} }
if (!this.isMediaMessage(message) && message.type !== 'sticker') { await this.prismaRepository.message.create({
await this.prismaRepository.message.create({ data: messageRaw,
data: messageRaw, });
});
}
const contact = await this.prismaRepository.contact.findFirst({ const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: key.remoteJid }, where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
@ -865,54 +702,17 @@ export class BusinessStartupService extends ChannelStartupService {
} }
protected async eventHandler(content: any) { protected async eventHandler(content: any) {
try { const database = this.configService.get<Database>('DATABASE');
// Registro para depuración const settings = await this.findSettings();
this.logger.log('Contenido recibido en eventHandler:');
this.logger.log(JSON.stringify(content, null, 2));
const database = this.configService.get<Database>('DATABASE'); this.messageHandle(content, database, settings);
const settings = await this.findSettings();
// Si hay mensajes, verificar primero el tipo
if (content.messages && content.messages.length > 0) {
const message = content.messages[0];
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
// Verificamos el tipo de mensaje antes de procesarlo
if (
message.type === 'text' ||
message.type === 'image' ||
message.type === 'video' ||
message.type === 'audio' ||
message.type === 'document' ||
message.type === 'sticker' ||
message.type === 'location' ||
message.type === 'contacts' ||
message.type === 'interactive' ||
message.type === 'button' ||
message.type === 'reaction'
) {
// Procesar el mensaje normalmente
this.messageHandle(content, database, settings);
} else {
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
}
} else if (content.statuses) {
// Procesar actualizaciones de estado
this.messageHandle(content, database, settings);
} else {
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
}
} catch (error) {
this.logger.error('Error en eventHandler:');
this.logger.error(error);
}
} }
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) { protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
try { try {
let quoted: any; let quoted: any;
let webhookUrl: any; let webhookUrl: any;
const linkPreview = options?.linkPreview != false ? undefined : false;
if (options?.quoted) { if (options?.quoted) {
const m = options?.quoted; const m = options?.quoted;
@ -980,15 +780,13 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''), to: number.replace(/\D/g, ''),
text: { text: {
body: message['conversation'], body: message['conversation'],
preview_url: Boolean(options?.linkPreview), preview_url: linkPreview,
}, },
}; };
quoted ? (content.context = { message_id: quoted.id }) : content; quoted ? (content.context = { message_id: quoted.id }) : content;
return await this.post(content, 'messages'); return await this.post(content, 'messages');
} }
if (message['media']) { if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
content = { content = {
messaging_product: 'whatsapp', messaging_product: 'whatsapp',
recipient_type: 'individual', recipient_type: 'individual',
@ -996,10 +794,8 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''), to: number.replace(/\D/g, ''),
[message['mediaType']]: { [message['mediaType']]: {
[message['type']]: message['id'], [message['type']]: message['id'],
...(message['mediaType'] !== 'audio' && preview_url: linkPreview,
message['fileName'] && caption: message['caption'],
!isImage && { filename: message['fileName'] }),
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
}, },
}; };
quoted ? (content.context = { message_id: quoted.id }) : content; quoted ? (content.context = { message_id: quoted.id }) : content;
@ -1097,13 +893,13 @@ export class BusinessStartupService extends ChannelStartupService {
} }
})(); })();
if (messageSent?.error_data || messageSent.message) { if (messageSent?.error_data) {
this.logger.error(messageSent); this.logger.error(messageSent);
return messageSent; return messageSent;
} }
const messageRaw: any = { const messageRaw: any = {
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) }, key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: this.createJid(number) },
message: this.convertMessageToRaw(message, content), message: this.convertMessageToRaw(message, content),
messageType: this.renderMessageType(content.type), messageType: this.renderMessageType(content.type),
messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000), messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
@ -1164,50 +960,29 @@ export class BusinessStartupService extends ChannelStartupService {
return res; return res;
} }
private async getIdMedia(mediaMessage: any, isFile = false) { private async getIdMedia(mediaMessage: any) {
try { const formData = new FormData();
const formData = new FormData();
if (isFile === false) { const fileStream = createReadStream(mediaMessage.media);
if (isURL(mediaMessage.media)) {
const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
const buffer = Buffer.from(response.data, 'base64');
formData.append('file', buffer, {
filename: mediaMessage.fileName || 'media',
contentType: mediaMessage.mimetype,
});
} else {
const buffer = Buffer.from(mediaMessage.media, 'base64');
formData.append('file', buffer, {
filename: mediaMessage.fileName || 'media',
contentType: mediaMessage.mimetype,
});
}
} else {
formData.append('file', mediaMessage.media.buffer, {
filename: mediaMessage.media.originalname,
contentType: mediaMessage.media.mimetype,
});
}
const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype; formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype);
formData.append('messaging_product', 'whatsapp');
formData.append('typeFile', mimetype); // const fileBuffer = await fs.readFile(mediaMessage.media);
formData.append('messaging_product', 'whatsapp');
const token = this.token; // const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype });
// formData.append('file', fileBlob);
// formData.append('typeFile', mediaMessage.mimetype);
// formData.append('messaging_product', 'whatsapp');
const headers = { Authorization: `Bearer ${token}` }; const headers = { Authorization: `Bearer ${this.token}` };
const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${ const res = await axios.post(
this.configService.get<WaBusiness>('WA_BUSINESS').VERSION process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media',
}/${this.number}/media`; formData,
{ headers },
const res = await axios.post(url, formData, { headers }); );
return res.data.id; return res.data.id;
} catch (error) {
this.logger.error(error.response.data);
throw new InternalServerErrorException(error?.toString() || error);
}
} }
protected async prepareMediaMessage(mediaMessage: MediaMessage) { protected async prepareMediaMessage(mediaMessage: MediaMessage) {
@ -1226,7 +1001,7 @@ export class BusinessStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4'; mediaMessage.fileName = 'video.mp4';
} }
let mimetype: string | false; let mimetype: string;
const prepareMedia: any = { const prepareMedia: any = {
caption: mediaMessage?.caption, caption: mediaMessage?.caption,
@ -1237,11 +1012,11 @@ export class BusinessStartupService extends ChannelStartupService {
}; };
if (isURL(mediaMessage.media)) { if (isURL(mediaMessage.media)) {
mimetype = mimeTypes.lookup(mediaMessage.media); mimetype = mime.getType(mediaMessage.media);
prepareMedia.id = mediaMessage.media; prepareMedia.id = mediaMessage.media;
prepareMedia.type = 'link'; prepareMedia.type = 'link';
} else { } else {
mimetype = mimeTypes.lookup(mediaMessage.fileName); mimetype = mime.getType(mediaMessage.fileName);
const id = await this.getIdMedia(prepareMedia); const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id; prepareMedia.id = id;
prepareMedia.type = 'id'; prepareMedia.type = 'id';
@ -1280,87 +1055,45 @@ export class BusinessStartupService extends ChannelStartupService {
return mediaSent; return mediaSent;
} }
public async processAudio(audio: string, number: string, file: any) { public async processAudio(audio: string, number: string) {
number = number.replace(/\D/g, ''); number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`; const hash = `${number}-${new Date().getTime()}`;
if (process.env.API_AUDIO_CONVERTER) { let mimetype: string;
this.logger.verbose('Using audio converter API');
const formData = new FormData();
if (file) { const prepareMedia: any = {
formData.append('file', file.buffer, { fileName: `${hash}.mp3`,
filename: file.originalname, mediaType: 'audio',
contentType: file.mimetype, media: audio,
}); };
} else if (isURL(audio)) {
formData.append('url', audio);
} else {
formData.append('base64', audio);
}
formData.append('format', 'mp3');
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
headers: {
...formData.getHeaders(),
apikey: process.env.API_AUDIO_CONVERTER_KEY,
},
});
const audioConverter = response?.data?.audio || response?.data?.url;
if (!audioConverter) {
throw new InternalServerErrorException('Failed to convert audio');
}
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audioConverter,
mimetype: 'audio/mpeg',
};
if (isURL(audio)) {
mimetype = mime.getType(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else {
mimetype = mime.getType(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia); const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id; prepareMedia.id = id;
prepareMedia.type = 'id'; prepareMedia.type = 'id';
this.logger.verbose('Audio converted');
return prepareMedia;
} else {
let mimetype: string | false;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else if (audio && !file) {
mimetype = mimeTypes.lookup(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
} else if (file) {
prepareMedia.media = file;
const id = await this.getIdMedia(prepareMedia, true);
prepareMedia.id = id;
prepareMedia.type = 'id';
mimetype = file.mimetype;
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
} }
prepareMedia.mimetype = mimetype;
return prepareMedia;
} }
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const message = await this.processAudio(data.audio, data.number, file); const mediaData: SendAudioDto = { ...data };
if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
}
const message = await this.processAudio(mediaData.audio, data.number);
const audioSent = await this.sendMessageWithTyping( const audioSent = await this.sendMessageWithTyping(
data.number, data.number,
@ -1379,42 +1112,97 @@ export class BusinessStartupService extends ChannelStartupService {
return audioSent; return audioSent;
} }
public async buttonMessage(data: SendButtonsDto) { private toJSONString(button: Button): string {
const embeddedMedia: any = {}; const toString = (obj: any) => JSON.stringify(obj);
const btnItems = { const json = {
text: data.buttons.map((btn) => btn.displayText), call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
ids: data.buttons.map((btn) => btn.id), reply: () => toString({ display_text: button.displayText, id: button.id }),
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
url: () =>
toString({
display_text: button.displayText,
url: button.url,
merchant_url: button.url,
}),
}; };
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { return json[button.type]?.() || '';
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); }
}
return await this.sendMessageWithTyping( private readonly mapType = new Map<TypeButton, string>([
data.number, ['reply', 'quick_reply'],
{ ['copy', 'cta_copy'],
text: !embeddedMedia?.mediaKey ? data.title : undefined, ['url', 'cta_url'],
buttons: data.buttons.map((button) => { ['call', 'cta_call'],
return { ]);
type: 'reply',
reply: { public async buttonMessage(data: SendButtonsDto) {
title: button.displayText, const generate = await (async () => {
id: button.id, if (data?.thumbnailUrl) {
return await this.prepareMediaMessage({
mediatype: 'image',
media: data.thumbnailUrl,
});
}
})();
const buttons = data.buttons.map((value) => {
return {
name: this.mapType.get(value.type),
buttonParamsJson: this.toJSONString(value),
};
});
const message: proto.IMessage = {
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2,
},
interactiveMessage: {
body: {
text: (() => {
let t = '*' + data.title + '*';
if (data?.description) {
t += '\n\n';
t += data.description;
t += '\n';
}
return t;
})(),
}, },
}; footer: {
}), text: data?.footer,
[embeddedMedia?.mediaKey]: embeddedMedia?.message, },
header: (() => {
if (generate?.message?.imageMessage) {
return {
hasMediaAttachment: !!generate.message.imageMessage,
imageMessage: generate.message.imageMessage,
};
}
})(),
nativeFlowMessage: {
buttons: buttons,
messageParamsJson: JSON.stringify({
from: 'api',
templateId: v4(),
}),
},
},
},
}, },
{ };
delay: data?.delay,
presence: 'composing', return await this.sendMessageWithTyping(data.number, message, {
quoted: data?.quoted, delay: data?.delay,
linkPreview: data?.linkPreview, presence: 'composing',
mentionsEveryOne: data?.mentionsEveryOne, quoted: data?.quoted,
mentioned: data?.mentioned, mentionsEveryOne: data?.mentionsEveryOne,
}, mentioned: data?.mentioned,
); });
} }
public async locationMessage(data: SendLocationDto) { public async locationMessage(data: SendLocationDto) {
@ -1522,7 +1310,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
if (!contact.wuid) { if (!contact.wuid) {
contact.wuid = createJid(contact.phoneNumber); contact.wuid = this.createJid(contact.phoneNumber);
} }
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';

View File

@ -1,60 +0,0 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BaileysController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async onWhatsapp({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysOnWhatsapp(body?.jid);
}
public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs);
}
public async assertSessions({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysAssertSessions(body?.jids, body?.force);
}
public async createParticipantNodes({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs);
}
public async getUSyncDevices({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices);
}
public async generateMessageTag({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGenerateMessageTag();
}
public async sendNode({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSendNode(body?.stanza);
}
public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext);
}
public async getAuthState({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetAuthState();
}
}

View File

@ -1,105 +0,0 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { baileysController } from '@api/server.module';
import { instanceSchema } from '@validate/instance.schema';
import { RequestHandler, Router } from 'express';
export class BaileysRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.onWhatsapp(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.profilePictureUrl(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('assertSessions'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.assertSessions(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.createParticipantNodes(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getUSyncDevices(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.generateMessageTag(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('sendNode'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.sendNode(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getAuthState'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getAuthState(instance),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -1,78 +0,0 @@
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
export interface ServerToClientEvents {
withAck: (d: string, callback: (e: number) => void) => void;
onWhatsApp: onWhatsAppType;
profilePictureUrl: ProfilePictureUrlType;
assertSessions: AssertSessionsType;
createParticipantNodes: CreateParticipantNodesType;
getUSyncDevices: GetUSyncDevicesType;
generateMessageTag: GenerateMessageTagType;
sendNode: SendNodeType;
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
}
export interface ClientToServerEvents {
init: (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'CB:call': (packet: any) => void;
'CB:ack,class:call': (packet: any) => void;
'connection.update:status': (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'connection.update:qr': (qr: string) => void;
}
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
export type onWhatsAppCallback = (
response: {
exists: boolean;
jid: string;
}[],
) => void;
export type ProfilePictureUrlType = (
jid: string,
type: 'image' | 'preview',
timeoutMs: number | undefined,
callback: ProfilePictureUrlCallback,
) => void;
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
export type AssertSessionsCallback = (response: boolean) => void;
export type CreateParticipantNodesType = (
jids: string[],
message: any,
extraAttrs: any,
callback: CreateParticipantNodesCallback,
) => void;
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
export type GetUSyncDevicesType = (
jids: string[],
useCache: boolean,
ignoreZeroDevices: boolean,
callback: GetUSyncDevicesTypeCallback,
) => void;
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
export type GenerateMessageTagTypeCallback = (response: string) => void;
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
export type SendNodeTypeCallback = (response: boolean) => void;
export type SignalRepositoryDecryptMessageType = (
jid: string,
type: 'pkmsg' | 'msg',
ciphertext: Buffer,
callback: SignalRepositoryDecryptMessageCallback,
) => void;
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;

View File

@ -1,181 +0,0 @@
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
import { io, Socket } from 'socket.io-client';
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
let baileys_connection_state: WAConnectionState = 'close';
export const useVoiceCallsBaileys = async (
wavoip_token: string,
baileys_sock: WASocket,
status?: WAConnectionState,
logger?: boolean,
) => {
baileys_connection_state = status ?? 'close';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
transports: ['websocket'],
path: `/${wavoip_token}/websocket`,
});
socket.on('connect', () => {
if (logger) console.log('[*] - Wavoip connected', socket.id);
socket.emit(
'init',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
baileys_connection_state,
);
});
socket.on('disconnect', () => {
if (logger) console.log('[*] - Wavoip disconnect');
});
socket.on('connect_error', (error) => {
if (socket.active) {
if (logger)
console.log(
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
error,
);
} else {
if (logger) console.log('[*] - Wavoip connection error', error.message);
}
});
socket.on('onWhatsApp', async (jid, callback) => {
try {
const response: any = await baileys_sock.onWhatsApp(jid);
callback(response);
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
} catch (error) {
if (logger) console.error('[*] Error on call onWhatsApp function', error);
}
});
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
try {
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
callback(response);
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
} catch (error) {
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
}
});
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids, force);
callback(response);
if (logger) console.log('[*] Success on call assertSessions function', response);
} catch (error) {
if (logger) console.error('[*] Error on call assertSessions function', error);
}
});
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
try {
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
callback(response, true);
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
} catch (error) {
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
}
});
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
try {
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
callback(response);
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
} catch (error) {
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
}
});
socket.on('generateMessageTag', async (callback) => {
try {
const response = await baileys_sock.generateMessageTag();
callback(response);
if (logger) console.log('[*] Success on call generateMessageTag function', response);
} catch (error) {
if (logger) console.error('[*] Error on call generateMessageTag function', error);
}
});
socket.on('sendNode', async (stanza, callback) => {
try {
console.log('sendNode', JSON.stringify(stanza));
const response = await baileys_sock.sendNode(stanza);
callback(true);
if (logger) console.log('[*] Success on call sendNode function', response);
} catch (error) {
if (logger) console.error('[*] Error on call sendNode function', error);
}
});
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
try {
const response = await baileys_sock.signalRepository.decryptMessage({
jid: jid,
type: type,
ciphertext: ciphertext,
});
callback(response);
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
} catch (error) {
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
}
});
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
const { connection } = update;
if (connection) {
baileys_connection_state = connection;
socket
.timeout(1000)
.emit(
'connection.update:status',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
connection,
);
}
if (update.qr) {
socket.timeout(1000).emit('connection.update:qr', update.qr);
}
});
baileys_sock.ws.on('CB:call', (packet) => {
if (logger) console.log('[*] Signling received');
socket.volatile.timeout(1000).emit('CB:call', packet);
});
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
if (logger) console.log('[*] Signling ack received');
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
});
return socket;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ import dayjs from 'dayjs';
import FormData from 'form-data'; import FormData from 'form-data';
import Jimp from 'jimp'; import Jimp from 'jimp';
import Long from 'long'; import Long from 'long';
import mimeTypes from 'mime-types'; import mime from 'mime';
import path from 'path'; import path from 'path';
import { Readable } from 'stream'; import { Readable } from 'stream';
@ -295,57 +295,51 @@ export class ChatwootService {
avatar_url?: string, avatar_url?: string,
jid?: string, jid?: string,
) { ) {
try { const client = await this.clientCw(instance);
const client = await this.clientCw(instance);
if (!client) { if (!client) {
this.logger.warn('client not found'); this.logger.warn('client not found');
return null;
}
let data: any = {};
if (!isGroup) {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: jid,
avatar_url: avatar_url,
};
if ((jid && jid.includes('@')) || !jid) {
data['phone_number'] = `+${phoneNumber}`;
}
} else {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: phoneNumber,
avatar_url: avatar_url,
};
}
const contact = await client.contacts.create({
accountId: this.provider.accountId,
data,
});
if (!contact) {
this.logger.warn('contact not found');
return null;
}
const findContact = await this.findContact(instance, phoneNumber);
const contactId = findContact?.id;
await this.addLabelToContact(this.provider.nameInbox, contactId);
return contact;
} catch (error) {
this.logger.error('Error creating contact');
console.log(error);
return null; return null;
} }
let data: any = {};
if (!isGroup) {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: jid,
avatar_url: avatar_url,
};
if ((jid && jid.includes('@')) || !jid) {
data['phone_number'] = `+${phoneNumber}`;
}
} else {
data = {
inbox_id: inboxId,
name: name || phoneNumber,
identifier: phoneNumber,
avatar_url: avatar_url,
};
}
const contact = await client.contacts.create({
accountId: this.provider.accountId,
data,
});
if (!contact) {
this.logger.warn('contact not found');
return null;
}
const findContact = await this.findContact(instance, phoneNumber);
const contactId = findContact?.id;
await this.addLabelToContact(this.provider.nameInbox, contactId);
return contact;
} }
public async updateContact(instance: InstanceDto, id: number, data: any) { public async updateContact(instance: InstanceDto, id: number, data: any) {
@ -407,6 +401,7 @@ export class ChatwootService {
return true; return true;
} catch (error) { } catch (error) {
this.logger.error(error);
return false; return false;
} }
} }
@ -549,240 +544,216 @@ export class ChatwootService {
} }
public async createConversation(instance: InstanceDto, body: any) { public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds
try { try {
// Processa atualização de contatos já criados @lid this.logger.verbose('--- Start createConversation ---');
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`,
);
await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`,
});
}
}
this.logger.verbose(`--- Start createConversation ---`);
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`); this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
// If it already exists in the cache, return conversationId const client = await this.clientCw(instance);
if (!client) {
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
return null;
}
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
this.logger.verbose(`Cache key: ${cacheKey}`);
if (await this.cache.has(cacheKey)) { if (await this.cache.has(cacheKey)) {
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
const conversationId = (await this.cache.get(cacheKey)) as number; const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`); this.logger.verbose(`Cached conversation ID: ${conversationId}`);
let conversationExists: conversation | boolean;
try {
conversationExists = await client.conversations.get({
accountId: this.provider.accountId,
conversationId: conversationId,
});
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
} catch (error) {
this.logger.error(`Error getting conversation: ${error}`);
conversationExists = false;
}
if (!conversationExists) {
this.logger.verbose('Conversation does not exist, re-calling createConversation');
this.cache.delete(cacheKey);
return await this.createConversation(instance, body);
}
return conversationId; return conversationId;
} }
// If lock already exists, wait until release or timeout const isGroup = body.key.remoteJid.includes('@g.us');
if (await this.cache.has(lockKey)) { this.logger.verbose(`Is group: ${isGroup}`);
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
const start = Date.now(); const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
while (await this.cache.has(lockKey)) { this.logger.verbose(`Chat ID: ${chatId}`);
if (Date.now() - start > maxWaitTime) {
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`); let nameContact: string;
break;
} nameContact = !body.key.fromMe ? body.pushName : chatId;
await new Promise((res) => setTimeout(res, 300)); this.logger.verbose(`Name contact: ${nameContact}`);
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number; const filterInbox = await this.getInbox(instance);
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
return conversationId; if (!filterInbox) {
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
return null;
}
if (isGroup) {
this.logger.verbose('Processing group conversation');
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
nameContact = `${group.subject} (GROUP)`;
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
body.key.participant.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
if (findParticipant) {
if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
name: body.pushName,
avatar_url: picture_url.profilePictureUrl || null,
});
} }
} else {
await this.createContact(
instance,
body.key.participant.split('@')[0],
filterInbox.id,
false,
body.pushName,
picture_url.profilePictureUrl || null,
body.key.participant,
);
} }
} }
// Adquire lock const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
await this.cache.set(lockKey, true, 30); this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
try { let contact = await this.findContact(instance, chatId);
/* this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
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 client = await this.clientCw(instance); if (contact) {
if (!client) return null; if (!body.key.fromMe) {
const waProfilePictureFile =
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
const isGroup = remoteJid.includes('@g.us'); this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0]; this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
let nameContact = !body.key.fromMe ? body.pushName : chatId;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
if (isGroup) { if (pictureNeedsUpdate || nameNeedsUpdate) {
this.logger.verbose(`Processing group conversation`); contact = await this.updateContact(instance, contact.id, {
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); ...(nameNeedsUpdate && { name: nameContact }),
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`); ...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
nameContact = `${group.subject} (GROUP)`; });
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
body.key.participant.split('@')[0],
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
if (findParticipant) {
if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
name: body.pushName,
avatar_url: picture_url.profilePictureUrl || null,
});
}
} else {
await this.createContact(
instance,
body.key.participant.split('@')[0],
filterInbox.id,
isGroup,
body.pushName,
picture_url.profilePictureUrl || null,
body.key.participant,
);
} }
} }
} else {
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); const jid = body.key.remoteJid;
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); contact = await this.createContact(
instance,
let contact = await this.findContact(instance, chatId); chatId,
filterInbox.id,
if (contact) { isGroup,
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`); nameContact,
if (!body.key.fromMe) { picture_url.profilePictureUrl || null,
const waProfilePictureFile = jid,
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
if (pictureNeedsUpdate || nameNeedsUpdate) {
contact = await this.updateContact(instance, contact.id, {
...(nameNeedsUpdate && { name: nameContact }),
...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
});
}
}
} else {
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact(
instance,
chatId,
filterInbox.id,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
);
}
if (!contact) {
this.logger.warn(`Contact not created or found`);
return null;
}
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
this.logger.verbose(`Contact ID: ${contactId}`);
const contactConversations = (await client.contacts.listConversations({
accountId: this.provider.accountId,
id: contactId,
})) as any;
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
if (!contactConversations || !contactConversations.payload) {
this.logger.error(`No conversations found or payload is undefined`);
return null;
}
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
); );
if (inboxConversation) { }
if (this.provider.reopenConversation) {
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`); if (!contact) {
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') { 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) {
if (conversation) {
await client.conversations.toggleStatus({ await client.conversations.toggleStatus({
accountId: this.provider.accountId, accountId: this.provider.accountId,
conversationId: inboxConversation.id, conversationId: conversation.id,
data: { data: {
status: 'pending', status: 'pending',
}, },
}); });
} }
} else {
inboxConversation = contactConversations.payload.find(
(conversation) =>
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
} }
} else {
conversation = contactConversations.payload.find(
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
} }
const data = { if (conversation) {
contact_id: contactId.toString(), this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
inbox_id: filterInbox.id.toString(), this.cache.set(cacheKey, conversation.id);
}; return conversation.id;
if (this.provider.conversationPending) {
data['status'] = 'pending';
} }
/*
Triple check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn(`Conversation not created or found`);
return null;
}
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} finally {
await this.cache.delete(lockKey);
this.logger.verbose(`Block released for: ${lockKey}`);
} }
const data = {
contact_id: contactId.toString(),
inbox_id: filterInbox.id.toString(),
};
if (this.provider.conversationPending) {
data['status'] = 'pending';
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn('Conversation not created or found');
return null;
}
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} catch (error) { } catch (error) {
this.logger.error(`Error in createConversation: ${error}`); this.logger.error(`Error in createConversation: ${error}`);
return null;
} }
} }
@ -961,12 +932,10 @@ export class ChatwootService {
quotedMsg?: MessageModel, quotedMsg?: MessageModel,
) { ) {
if (sourceId && this.isImportHistoryAvailable()) { if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId); const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
if (messageAlreadySaved) { if (messageAlreadySaved.size > 0) {
if (messageAlreadySaved.size > 0) { this.logger.warn('Message already saved on chatwoot');
this.logger.warn('Message already saved on chatwoot'); return null;
return null;
}
} }
} }
const data = new FormData(); const data = new FormData();
@ -1096,7 +1065,7 @@ export class ChatwootService {
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
try { try {
const parsedMedia = path.parse(decodeURIComponent(media)); const parsedMedia = path.parse(decodeURIComponent(media));
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || ''; let mimeType = mime.getType(parsedMedia?.ext) || '';
let fileName = parsedMedia?.name + parsedMedia?.ext; let fileName = parsedMedia?.name + parsedMedia?.ext;
if (!mimeType) { if (!mimeType) {
@ -1136,13 +1105,12 @@ export class ChatwootService {
sendTelemetry('/message/sendWhatsAppAudio'); sendTelemetry('/message/sendWhatsAppAudio');
const messageSent = await waInstance?.audioWhatsapp(data, null, true); const messageSent = await waInstance?.audioWhatsapp(data, true);
return messageSent; return messageSent;
} }
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif']; if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
type = 'document'; type = 'document';
} }
@ -1684,7 +1652,7 @@ export class ChatwootService {
stickerMessage: undefined, stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption, documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined, audioMessage: msg.audioMessage?.caption,
contactMessage: msg.contactMessage?.vcard, contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage, contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage, locationMessage: msg.locationMessage,
@ -1929,7 +1897,7 @@ export class ChatwootService {
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~') .replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage; : originalMessage;
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) { if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
return; return;
} }
@ -1989,7 +1957,7 @@ export class ChatwootService {
} }
if (!nameFile) { if (!nameFile) {
nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`; nameFile = `${Math.random().toString(36).substring(7)}.${mime.getExtension(downloadBase64.mimetype) || ''}`;
} }
const fileData = Buffer.from(downloadBase64.base64, 'base64'); const fileData = Buffer.from(downloadBase64.base64, 'base64');
@ -2001,21 +1969,11 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) { if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName; const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string; let content: string;
if (!body.key.fromMe) { if (!body.key.fromMe) {
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`; content = `**${participantName}:**\n\n${bodyMessage}`;
} else { } else {
content = `${bodyMessage}`; content = `${bodyMessage}`;
} }
@ -2088,8 +2046,8 @@ export class ChatwootService {
if (isAdsMessage) { if (isAdsMessage) {
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
const extension = mimeTypes.extension(imgBuffer.headers['content-type']); const extension = mime.getExtension(imgBuffer.headers['content-type']);
const mimeType = extension && mimeTypes.lookup(extension); const mimeType = extension && mime.getType(extension);
if (!mimeType) { if (!mimeType) {
this.logger.warn('mimetype of Ads message not found'); this.logger.warn('mimetype of Ads message not found');
@ -2097,7 +2055,7 @@ export class ChatwootService {
} }
const random = Math.random().toString(36).substring(7); const random = Math.random().toString(36).substring(7);
const nameFile = `${random}.${mimeTypes.extension(mimeType)}`; const nameFile = `${random}.${mime.getExtension(mimeType)}`;
const fileData = Buffer.from(imgBuffer.data, 'binary'); const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData); const img = await Jimp.read(fileData);
@ -2140,21 +2098,11 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) { if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName; const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string; let content: string;
if (!body.key.fromMe) { if (!body.key.fromMe) {
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`; content = `**${participantName}**\n\n${bodyMessage}`;
} else { } else {
content = `${bodyMessage}`; content = `${bodyMessage}`;
} }
@ -2230,7 +2178,7 @@ export class ChatwootService {
} }
} }
if (event === 'messages.edit' || event === 'send.message.update') { if (event === 'messages.edit') {
const editedText = `${ const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`; }\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
@ -2494,61 +2442,57 @@ export class ChatwootService {
chatwootConfig: ChatwootDto, chatwootConfig: ChatwootDto,
prepareMessage: (message: any) => any, prepareMessage: (message: any) => any,
) { ) {
try { if (!this.isImportHistoryAvailable()) {
if (!this.isImportHistoryAvailable()) {
return;
}
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
return;
}
const inbox = await this.getInbox(instance);
const sqlMessages = `select * from messages m
where account_id = ${chatwootConfig.accountId}
and inbox_id = ${inbox.id}
and created_at >= now() - interval '6h'
order by created_at desc`;
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
const ids: string[] = messagesData
.filter((message) => !!message.source_id)
.map((message) => message.source_id.replace('WAID:', ''));
const savedMessages = await this.prismaRepository.message.findMany({
where: {
Instance: { name: instance.instanceName },
messageTimestamp: { gte: dayjs().subtract(6, 'hours').unix() },
AND: ids.map((id) => ({ key: { path: ['id'], not: id } })),
},
});
const filteredMessages = savedMessages.filter(
(msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid),
);
const messagesRaw: any[] = [];
for (const m of filteredMessages) {
if (!m.message || !m.key || !m.messageTimestamp) {
continue;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
messagesRaw.push(prepareMessage(m as any));
}
this.addHistoryMessages(
instance,
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
);
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
} catch (error) {
return; return;
} }
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
return;
}
const inbox = await this.getInbox(instance);
const sqlMessages = `select * from messages m
where account_id = ${chatwootConfig.accountId}
and inbox_id = ${inbox.id}
and created_at >= now() - interval '6h'
order by created_at desc`;
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
const ids: string[] = messagesData
.filter((message) => !!message.source_id)
.map((message) => message.source_id.replace('WAID:', ''));
const savedMessages = await this.prismaRepository.message.findMany({
where: {
Instance: { name: instance.instanceName },
messageTimestamp: { gte: dayjs().subtract(6, 'hours').unix() },
AND: ids.map((id) => ({ key: { path: ['id'], not: id } })),
},
});
const filteredMessages = savedMessages.filter(
(msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid),
);
const messagesRaw: any[] = [];
for (const m of filteredMessages) {
if (!m.message || !m.key || !m.messageTimestamp) {
continue;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
messagesRaw.push(prepareMessage(m as any));
}
this.addHistoryMessages(
instance,
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
);
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Flowise } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions'; import { Flowise } from '@prisma/client';
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client'; import { getConversationMessage } from '@utils/getConversationMessage';
import { BaseChatbotController } from '../../base-chatbot.controller'; import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { FlowiseDto } from '../dto/flowise.dto'; import { FlowiseDto } from '../dto/flowise.dto';
import { FlowiseService } from '../services/flowise.service'; import { FlowiseService } from '../services/flowise.service';
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> { export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
constructor( constructor(
private readonly flowiseService: FlowiseService, private readonly flowiseService: FlowiseService,
prismaRepository: PrismaRepository, prismaRepository: PrismaRepository,
@ -24,73 +24,15 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
} }
public readonly logger = new Logger('FlowiseController'); public readonly logger = new Logger('FlowiseController');
protected readonly integrationName = 'Flowise';
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED; integrationEnabled: boolean;
botRepository: any; botRepository: any;
settingsRepository: any; settingsRepository: any;
sessionRepository: any; sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
protected getFallbackBotId(settings: any): string | undefined { // Bots
return settings?.flowiseIdFallback;
}
protected getFallbackFieldName(): string {
return 'flowiseIdFallback';
}
protected getIntegrationType(): string {
return 'flowise';
}
protected getAdditionalBotData(data: FlowiseDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
return {
apiUrl: data.apiUrl,
apiKey: data.apiKey,
};
}
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: { not: botId },
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Flowise already exists');
}
}
// Process Flowise-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: FlowiseModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
// Override createBot to add module availability check and Flowise-specific validation
public async createBot(instance: InstanceDto, data: FlowiseDto) { public async createBot(instance: InstanceDto, data: FlowiseDto) {
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
const instanceId = await this.prismaRepository.instance const instanceId = await this.prismaRepository.instance
.findFirst({ .findFirst({
where: { where: {
@ -99,7 +41,66 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
}) })
.then((instance) => instance.id); .then((instance) => instance.id);
// Flowise-specific duplicate check if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids ||
!data.splitMessages ||
!data.timePerChar
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
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 (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a Flowise with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({ const checkDuplicate = await this.botRepository.findFirst({
where: { where: {
instanceId: instanceId, instanceId: instanceId,
@ -112,7 +113,746 @@ export class FlowiseController extends BaseChatbotController<FlowiseModel, Flowi
throw new Error('Flowise already exists'); throw new Error('Flowise already exists');
} }
// Let the base class handle the rest if (data.triggerType === 'keyword') {
return super.createBot(instance, data); 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,
this.settingsRepository,
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) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.flowiseService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
debouncedContent,
msg?.pushName,
);
});
} else {
await this.flowiseService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,86 +1,21 @@
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Auth, ConfigService, HttpServer, Typebot } from '@config/env.config'; import { Auth, ConfigService, HttpServer, Typebot } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Instance, IntegrationSession, Message, Typebot as TypebotModel } from '@prisma/client'; import { Instance, IntegrationSession, Message, Typebot as TypebotModel } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage'; import { getConversationMessage } from '@utils/getConversationMessage';
import { sendTelemetry } from '@utils/sendTelemetry'; import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios'; import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service'; export class TypebotService {
import { OpenaiService } from '../../openai/services/openai.service';
export class TypebotService extends BaseChatbotService<TypebotModel, any> {
private openaiService: OpenaiService;
constructor( constructor(
waMonitor: WAMonitoringService, private readonly waMonitor: WAMonitoringService,
configService: ConfigService, private readonly configService: ConfigService,
prismaRepository: PrismaRepository, private readonly prismaRepository: PrismaRepository,
openaiService: OpenaiService, ) {}
) {
super(waMonitor, prismaRepository, 'TypebotService', configService);
this.openaiService = openaiService;
}
/** private readonly logger = new Logger('TypebotService');
* Get the bot type identifier
*/
protected getBotType(): string {
return 'typebot';
}
/**
* Base class wrapper - calls the original processTypebot method
*/
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: any,
bot: TypebotModel,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void> {
// Map the base class call to the original processTypebot method
await this.processTypebot(
instance,
remoteJid,
msg,
session,
bot,
bot.url,
settings.expire,
bot.typebot,
settings.keywordFinish,
settings.delayMessage,
settings.unknownMessage,
settings.listeningFromMe,
settings.stopBotFromMe,
settings.keepOpen,
content,
);
}
/**
* Simplified wrapper for controller compatibility
*/
public async processTypebotSimple(
instance: any,
remoteJid: string,
bot: TypebotModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
return this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
/**
* Create a new TypeBot session with prefilled variables
*/
public async createNewSession(instance: Instance, data: any) { public async createNewSession(instance: Instance, data: any) {
if (data.remoteJid === 'status@broadcast') return; if (data.remoteJid === 'status@broadcast') return;
const id = Math.floor(Math.random() * 10000000000).toString(); const id = Math.floor(Math.random() * 10000000000).toString();
@ -142,12 +77,8 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
}, },
awaitUser: false, awaitUser: false,
botId: data.botId, botId: data.botId,
instanceId: instance.id,
type: 'typebot', type: 'typebot',
Instance: {
connect: {
id: instance.id,
},
},
}, },
}); });
} }
@ -158,11 +89,8 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
} }
} }
/**
* Send WhatsApp message with complex TypeBot formatting
*/
public async sendWAMessage( public async sendWAMessage(
instanceDb: Instance, instance: Instance,
session: IntegrationSession, session: IntegrationSession,
settings: { settings: {
expire: number; expire: number;
@ -178,107 +106,20 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
input: any, input: any,
clientSideActions: any, clientSideActions: any,
) { ) {
const waInstance = this.waMonitor.waInstances[instanceDb.name]; processMessages(
await this.processMessages( this.waMonitor.waInstances[instance.name],
waInstance,
session, session,
settings, settings,
messages, messages,
input, input,
clientSideActions, clientSideActions,
this.applyFormatting, applyFormatting,
this.prismaRepository, this.prismaRepository,
).catch((err) => { ).catch((err) => {
console.error('Erro ao processar mensagens:', err); console.error('Erro ao processar mensagens:', err);
}); });
}
/** function findItemAndGetSecondsToWait(array, targetId) {
* Apply rich text formatting for TypeBot messages
*/
private applyFormatting(element: any): string {
let text = '';
if (element.text) {
text += element.text;
}
if (element.children && element.type !== 'a') {
for (const child of element.children) {
text += this.applyFormatting(child);
}
}
if (element.type === 'p' && element.type !== 'inline-variable') {
text = text.trim() + '\n';
}
if (element.type === 'inline-variable') {
text = text.trim();
}
if (element.type === 'ol') {
text =
'\n' +
text
.split('\n')
.map((line, index) => (line ? `${index + 1}. ${line}` : ''))
.join('\n');
}
if (element.type === 'li') {
text = text
.split('\n')
.map((line) => (line ? ` ${line}` : ''))
.join('\n');
}
let formats = '';
if (element.bold) {
formats += '*';
}
if (element.italic) {
formats += '_';
}
if (element.underline) {
formats += '~';
}
let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`;
if (element.url) {
formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`;
}
return formattedText;
}
/**
* Process TypeBot messages with full feature support
*/
private async processMessages(
instance: any,
session: IntegrationSession,
settings: {
expire: number;
keywordFinish: string;
delayMessage: number;
unknownMessage: string;
listeningFromMe: boolean;
stopBotFromMe: boolean;
keepOpen: boolean;
},
messages: any,
input: any,
clientSideActions: any,
applyFormatting: any,
prismaRepository: PrismaRepository,
) {
// Helper function to find wait time
const findItemAndGetSecondsToWait = (array: any[], targetId: string) => {
if (!array) return null; if (!array) return null;
for (const item of array) { for (const item of array) {
@ -287,266 +128,220 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
} }
} }
return null; return null;
};
for (const message of messages) {
if (message.type === 'text') {
let formattedText = '';
for (const richText of message.content.richText) {
for (const element of richText.children) {
formattedText += applyFormatting(element);
}
formattedText += '\n';
}
formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
formattedText = formattedText.replace(/\n$/, '');
if (formattedText.includes('[list]')) {
await this.processListMessage(instance, formattedText, session.remoteJid);
} else if (formattedText.includes('[buttons]')) {
await this.processButtonMessage(instance, formattedText, session.remoteJid);
} else {
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
}
sendTelemetry('/message/sendText');
}
if (message.type === 'image') {
await instance.mediaMessage(
{
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'image',
media: message.content.url,
},
null,
false,
);
sendTelemetry('/message/sendMedia');
}
if (message.type === 'video') {
await instance.mediaMessage(
{
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'video',
media: message.content.url,
},
null,
false,
);
sendTelemetry('/message/sendMedia');
}
if (message.type === 'audio') {
await instance.audioWhatsapp(
{
number: session.remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
encoding: true,
audio: message.content.url,
},
false,
);
sendTelemetry('/message/sendWhatsAppAudio');
}
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
if (wait) {
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
} }
// Process input choices function applyFormatting(element) {
if (input) { let text = '';
if (input.type === 'choice input') {
let formattedText = '';
const items = input.items; if (element.text) {
text += element.text;
for (const item of items) {
formattedText += `▶️ ${item.content}\n`;
}
formattedText = formattedText.replace(/\n$/, '');
if (formattedText.includes('[list]')) {
await this.processListMessage(instance, formattedText, session.remoteJid);
} else if (formattedText.includes('[buttons]')) {
await this.processButtonMessage(instance, formattedText, session.remoteJid);
} else {
await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings);
}
sendTelemetry('/message/sendText');
} }
await prismaRepository.integrationSession.update({ if (element.children && element.type !== 'a') {
where: { for (const child of element.children) {
id: session.id, text += applyFormatting(child);
}, }
data: { }
awaitUser: true,
}, if (element.type === 'p' && element.type !== 'inline-variable') {
}); text = text.trim() + '\n';
} else { }
if (!settings?.keepOpen) {
await prismaRepository.integrationSession.deleteMany({ if (element.type === 'inline-variable') {
where: { text = text.trim();
id: session.id, }
},
}); if (element.type === 'ol') {
} else { text =
'\n' +
text
.split('\n')
.map((line, index) => (line ? `${index + 1}. ${line}` : ''))
.join('\n');
}
if (element.type === 'li') {
text = text
.split('\n')
.map((line) => (line ? ` ${line}` : ''))
.join('\n');
}
let formats = '';
if (element.bold) {
formats += '*';
}
if (element.italic) {
formats += '_';
}
if (element.underline) {
formats += '~';
}
let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`;
if (element.url) {
formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`;
}
return formattedText;
}
async function processMessages(
instance: any,
session: IntegrationSession,
settings: {
expire: number;
keywordFinish: string;
delayMessage: number;
unknownMessage: string;
listeningFromMe: boolean;
stopBotFromMe: boolean;
keepOpen: boolean;
},
messages: any,
input: any,
clientSideActions: any,
applyFormatting: any,
prismaRepository: PrismaRepository,
) {
for (const message of messages) {
if (message.type === 'text') {
let formattedText = '';
for (const richText of message.content.richText) {
for (const element of richText.children) {
formattedText += applyFormatting(element);
}
formattedText += '\n';
}
formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
sendTelemetry('/message/sendText');
}
if (message.type === 'image') {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'image',
media: message.content.url,
},
null,
false,
);
sendTelemetry('/message/sendMedia');
}
if (message.type === 'video') {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'video',
media: message.content.url,
},
null,
false,
);
sendTelemetry('/message/sendMedia');
}
if (message.type === 'audio') {
await instance.audioWhatsapp(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
encoding: true,
audio: message.content.url,
},
false,
);
sendTelemetry('/message/sendWhatsAppAudio');
}
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
if (wait) {
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}
console.log('input', input);
if (input) {
if (input.type === 'choice input') {
let formattedText = '';
const items = input.items;
for (const item of items) {
formattedText += `▶️ ${item.content}\n`;
}
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
sendTelemetry('/message/sendText');
}
await prismaRepository.integrationSession.update({ await prismaRepository.integrationSession.update({
where: { where: {
id: session.id, id: session.id,
}, },
data: { data: {
status: 'closed', awaitUser: true,
}, },
}); });
} } else {
} if (!settings?.keepOpen) {
} await prismaRepository.integrationSession.deleteMany({
where: {
/** id: session.id,
* Process list messages for WhatsApp },
*/ });
private async processListMessage(instance: any, formattedText: string, remoteJid: string) { } else {
const listJson = { await prismaRepository.integrationSession.update({
number: remoteJid.split('@')[0], where: {
title: '', id: session.id,
description: '', },
buttonText: '', data: {
footerText: '', status: 'closed',
sections: [], },
}; });
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/);
const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/);
const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/);
if (titleMatch) listJson.title = titleMatch[1].trim();
if (descriptionMatch) listJson.description = descriptionMatch[1].trim();
if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim();
if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim();
const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1];
if (menuContent) {
const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g);
if (sections) {
sections.forEach((section) => {
const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim();
const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g);
const sectionData = {
title: sectionTitle,
rows:
rows?.map((row) => ({
title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(),
description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(),
rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(),
})) || [],
};
listJson.sections.push(sectionData);
});
}
}
await instance.listMessage(listJson);
}
/**
* Process button messages for WhatsApp
*/
private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) {
const buttonJson = {
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
footer: '',
buttons: [],
};
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
} }
} }
} }
await instance.buttonMessage(buttonJson);
} }
/**
* Original TypeBot processing method with full functionality
*/
public async processTypebot( public async processTypebot(
waInstance: any, instance: Instance,
remoteJid: string, remoteJid: string,
msg: Message, msg: Message,
session: IntegrationSession, session: IntegrationSession,
@ -563,22 +358,13 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
content: string, content: string,
prefilledVariables?: any, prefilledVariables?: any,
) { ) {
// Get the database instance record
const instance = await this.prismaRepository.instance.findFirst({
where: {
name: waInstance.instanceName,
},
});
if (!instance) {
this.logger.error('Instance not found in database');
return;
}
// Handle session expiration
if (session && expire && expire > 0) { if (session && expire && expire > 0) {
const now = Date.now(); const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime(); const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt; const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60); const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > expire) { if (diffInMinutes > expire) {
@ -615,24 +401,24 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
prefilledVariables: prefilledVariables, prefilledVariables: prefilledVariables,
}); });
if (data?.session) { if (data.session) {
session = data.session; session = data.session;
} }
if (!data?.messages || data.messages.length === 0) { if (data.messages.length === 0) {
const content = getConversationMessage(msg.message); const content = getConversationMessage(msg.message);
if (!content) { if (!content) {
if (unknownMessage) { if (unknownMessage) {
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { this.waMonitor.waInstances[instance.name].textMessage(
delayMessage, {
expire, number: remoteJid.split('@')[0],
keywordFinish, delay: delayMessage || 1000,
listeningFromMe, text: unknownMessage,
stopBotFromMe, },
keepOpen, false,
unknownMessage, );
});
sendTelemetry('/message/sendText'); sendTelemetry('/message/sendText');
} }
return; return;
@ -664,7 +450,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
let urlTypebot: string; let urlTypebot: string;
let reqData: {}; let reqData: {};
if (version === 'latest') { if (version === 'latest') {
urlTypebot = `${url}/api/v1/sessions/${data?.sessionId}/continueChat`; urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
reqData = { reqData = {
message: content, message: content,
}; };
@ -672,7 +458,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
urlTypebot = `${url}/api/v1/sendMessage`; urlTypebot = `${url}/api/v1/sendMessage`;
reqData = { reqData = {
message: content, message: content,
sessionId: data?.sessionId, sessionId: data.sessionId,
}; };
} }
@ -691,9 +477,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
keepOpen: keepOpen, keepOpen: keepOpen,
}, },
remoteJid, remoteJid,
request?.data?.messages, request.data.messages,
request?.data?.input, request.data.input,
request?.data?.clientSideActions, request.data.clientSideActions,
); );
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -701,25 +487,23 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
} }
} }
if (data?.messages && data.messages.length > 0) { await this.sendWAMessage(
await this.sendWAMessage( instance,
instance, session,
session, {
{ expire: expire,
expire: expire, keywordFinish: keywordFinish,
keywordFinish: keywordFinish, delayMessage: delayMessage,
delayMessage: delayMessage, unknownMessage: unknownMessage,
unknownMessage: unknownMessage, listeningFromMe: listeningFromMe,
listeningFromMe: listeningFromMe, stopBotFromMe: stopBotFromMe,
stopBotFromMe: stopBotFromMe, keepOpen: keepOpen,
keepOpen: keepOpen, },
}, remoteJid,
remoteJid, data.messages,
data.messages, data.input,
data.input, data.clientSideActions,
data.clientSideActions, );
);
}
return; return;
} }
@ -729,7 +513,6 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
return; return;
} }
// Handle new sessions
if (!session) { if (!session) {
const data = await this.createNewSession(instance, { const data = await this.createNewSession(instance, {
enabled: findTypebot?.enabled, enabled: findTypebot?.enabled,
@ -750,38 +533,36 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
session = data.session; session = data.session;
} }
if (data?.messages && data.messages.length > 0) { await this.sendWAMessage(
await this.sendWAMessage( instance,
instance, session,
session, {
{ expire: expire,
expire: expire, keywordFinish: keywordFinish,
keywordFinish: keywordFinish, delayMessage: delayMessage,
delayMessage: delayMessage, unknownMessage: unknownMessage,
unknownMessage: unknownMessage, listeningFromMe: listeningFromMe,
listeningFromMe: listeningFromMe, stopBotFromMe: stopBotFromMe,
stopBotFromMe: stopBotFromMe, keepOpen: keepOpen,
keepOpen: keepOpen, },
}, remoteJid,
remoteJid, data?.messages,
data.messages, data?.input,
data.input, data?.clientSideActions,
data.clientSideActions, );
);
}
if (!data?.messages || data.messages.length === 0) { if (data.messages.length === 0) {
if (!content) { if (!content) {
if (unknownMessage) { if (unknownMessage) {
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { this.waMonitor.waInstances[instance.name].textMessage(
delayMessage, {
expire, number: remoteJid.split('@')[0],
keywordFinish, delay: delayMessage || 1000,
listeningFromMe, text: unknownMessage,
stopBotFromMe, },
keepOpen, false,
unknownMessage, );
});
sendTelemetry('/message/sendText'); sendTelemetry('/message/sendText');
} }
return; return;
@ -815,7 +596,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
let urlTypebot: string; let urlTypebot: string;
let reqData: {}; let reqData: {};
if (version === 'latest') { if (version === 'latest') {
urlTypebot = `${url}/api/v1/sessions/${data?.sessionId}/continueChat`; urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
reqData = { reqData = {
message: content, message: content,
}; };
@ -823,7 +604,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
urlTypebot = `${url}/api/v1/sendMessage`; urlTypebot = `${url}/api/v1/sendMessage`;
reqData = { reqData = {
message: content, message: content,
sessionId: data?.sessionId, sessionId: data.sessionId,
}; };
} }
request = await axios.post(urlTypebot, reqData); request = await axios.post(urlTypebot, reqData);
@ -841,9 +622,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
keepOpen: keepOpen, keepOpen: keepOpen,
}, },
remoteJid, remoteJid,
request?.data?.messages, request.data.messages,
request?.data?.input, request.data.input,
request?.data?.clientSideActions, request.data.clientSideActions,
); );
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -853,7 +634,6 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
return; return;
} }
// Update existing session
await this.prismaRepository.integrationSession.update({ await this.prismaRepository.integrationSession.update({
where: { where: {
id: session.id, id: session.id,
@ -866,15 +646,15 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
if (!content) { if (!content) {
if (unknownMessage) { if (unknownMessage) {
await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { this.waMonitor.waInstances[instance.name].textMessage(
delayMessage, {
expire, number: remoteJid.split('@')[0],
keywordFinish, delay: delayMessage || 1000,
listeningFromMe, text: unknownMessage,
stopBotFromMe, },
keepOpen, false,
unknownMessage, );
});
sendTelemetry('/message/sendText'); sendTelemetry('/message/sendText');
} }
return; return;
@ -901,10 +681,9 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
return; return;
} }
// Continue existing chat
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION; const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
let urlTypebot: string; let urlTypebot: string;
let reqData: { message: string; sessionId?: string }; let reqData: {};
if (version === 'latest') { if (version === 'latest') {
urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`; urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`;
reqData = { reqData = {
@ -917,20 +696,6 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
sessionId: session.sessionId.split('-')[1], sessionId: session.sessionId.split('-')[1],
}; };
} }
// Handle audio transcription if OpenAI service is available
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[TypeBot] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
reqData.message = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[TypeBot] Failed to transcribe audio: ${err}`);
}
}
const request = await axios.post(urlTypebot, reqData); const request = await axios.post(urlTypebot, reqData);
await this.sendWAMessage( await this.sendWAMessage(

View File

@ -13,7 +13,6 @@ export type EmitData = {
sender: string; sender: string;
apiKey?: string; apiKey?: string;
local?: boolean; local?: boolean;
integration?: string[];
}; };
export interface EventControllerInterface { export interface EventControllerInterface {
@ -24,7 +23,7 @@ export interface EventControllerInterface {
export class EventController { export class EventController {
public prismaRepository: PrismaRepository; public prismaRepository: PrismaRepository;
protected waMonitor: WAMonitoringService; private waMonitor: WAMonitoringService;
private integrationStatus: boolean; private integrationStatus: boolean;
private integrationName: string; private integrationName: string;
@ -132,7 +131,6 @@ export class EventController {
'MESSAGES_UPDATE', 'MESSAGES_UPDATE',
'MESSAGES_DELETE', 'MESSAGES_DELETE',
'SEND_MESSAGE', 'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET', 'CONTACTS_SET',
'CONTACTS_UPSERT', 'CONTACTS_UPSERT',
'CONTACTS_UPDATE', 'CONTACTS_UPDATE',
@ -152,8 +150,5 @@ export class EventController {
'TYPEBOT_CHANGE_STATUS', 'TYPEBOT_CHANGE_STATUS',
'REMOVE_INSTANCE', 'REMOVE_INSTANCE',
'LOGOUT_INSTANCE', 'LOGOUT_INSTANCE',
'INSTANCE_CREATE',
'INSTANCE_DELETE',
'STATUS_INSTANCE',
]; ];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -120,11 +120,7 @@ export class PusherController extends EventController implements EventController
sender, sender,
apiKey, apiKey,
local, local,
integration,
}: EmitData): Promise<void> { }: EmitData): Promise<void> {
if (integration && !integration.includes('pusher')) {
return;
}
if (!this.status) { if (!this.status) {
return; return;
} }

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