mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-23 17:08:44 -06:00
Merge branch 'develop' into feat/add-async-lock
This commit is contained in:
commit
d98fa5259e
@ -1,7 +1,6 @@
|
||||
.git
|
||||
*Dockerfile*
|
||||
*docker-compose*
|
||||
package-lock.json
|
||||
.env
|
||||
node_modules
|
||||
dist
|
10
.env.example
10
.env.example
@ -27,13 +27,17 @@ EVENT_EMITTER_MAX_LISTENERS=50
|
||||
# If you don't even want an expiration, enter the value false
|
||||
DEL_INSTANCE=false
|
||||
|
||||
# Provider: postgresql | mysql
|
||||
# Provider: postgresql | mysql | psql_bouncer
|
||||
DATABASE_PROVIDER=postgresql
|
||||
DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution?schema=public'
|
||||
DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution_db?schema=evolution_api'
|
||||
# Client name for the database connection
|
||||
# It is used to separate an API installation from another that uses the same database.
|
||||
DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange
|
||||
|
||||
# Bouncer connection: used only when the database provider is set to 'psql_bouncer'.
|
||||
# Defines the PostgreSQL URL with pgbouncer enabled (pgbouncer=true).
|
||||
# DATABASE_BOUNCER_CONNECTION_URI=postgresql://user:pass@pgbouncer:5432/evolution_db?pgbouncer=true&schema=evolution_api
|
||||
|
||||
# Choose the data you want to save in the application's database
|
||||
DATABASE_SAVE_DATA_INSTANCE=true
|
||||
DATABASE_SAVE_DATA_NEW_MESSAGE=true
|
||||
@ -196,7 +200,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
# Whatsapp Web version for baileys channel
|
||||
# https://web.whatsapp.com/check-update?version=0&platform=web
|
||||
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
|
||||
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,3 +1,14 @@
|
||||
# 2.3.1 (develop)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Update Baileys Version
|
||||
* Update Dockerhub Repository and Delete Config Session Variable
|
||||
* Fixed sending variables in typebot
|
||||
* Add unreadMessages in the response
|
||||
* Phone number as message ID for Evo AI
|
||||
* Fix upload to s3 when media message
|
||||
|
||||
# 2.3.0 (2025-06-17 09:19)
|
||||
|
||||
### Feature
|
||||
|
@ -6,7 +6,7 @@ if [ "$DOCKER_ENV" != "true" ]; then
|
||||
export_env_vars
|
||||
fi
|
||||
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" ]]; then
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then
|
||||
export DATABASE_URL
|
||||
echo "Deploying migrations for $DATABASE_PROVIDER"
|
||||
echo "Database URL: $DATABASE_URL"
|
||||
|
@ -6,7 +6,7 @@ if [ "$DOCKER_ENV" != "true" ]; then
|
||||
export_env_vars
|
||||
fi
|
||||
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" ]]; then
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then
|
||||
export DATABASE_URL
|
||||
echo "Generating database for $DATABASE_PROVIDER"
|
||||
echo "Database URL: $DATABASE_URL"
|
||||
@ -20,4 +20,4 @@ if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" ]]
|
||||
else
|
||||
echo "Error: Database provider $DATABASE_PROVIDER invalid."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
@ -2,7 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
evolution_v2:
|
||||
image: atendai/evolution-api:v2.2.3
|
||||
image: evoapicloud/evolution-api:v2.3.1
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
@ -94,7 +94,7 @@ services:
|
||||
- WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
|
||||
- CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
|
||||
#- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
|
||||
- QRCODE_LIMIT=30
|
||||
- OPENAI_ENABLED=true
|
||||
- DIFY_ENABLED=true
|
||||
|
12
Dockerfile
12
Dockerfile
@ -3,15 +3,17 @@ FROM node:20-alpine AS builder
|
||||
RUN apk update && \
|
||||
apk add --no-cache git ffmpeg wget curl bash openssl
|
||||
|
||||
LABEL version="2.3.0" description="Api to control whatsapp features through http requests."
|
||||
LABEL version="2.3.1" description="Api to control whatsapp features through http requests."
|
||||
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
|
||||
LABEL contact="contato@evolution-api.com"
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
COPY ./package.json ./tsconfig.json ./
|
||||
COPY ./package*.json ./
|
||||
COPY ./tsconfig.json ./
|
||||
COPY ./tsup.config.ts ./
|
||||
|
||||
RUN npm install
|
||||
RUN npm ci --silent
|
||||
|
||||
COPY ./src ./src
|
||||
COPY ./public ./public
|
||||
@ -19,7 +21,6 @@ COPY ./prisma ./prisma
|
||||
COPY ./manager ./manager
|
||||
COPY ./.env.example ./.env
|
||||
COPY ./runWithProvider.js ./
|
||||
COPY ./tsup.config.ts ./
|
||||
|
||||
COPY ./Docker ./Docker
|
||||
|
||||
@ -35,6 +36,7 @@ RUN apk update && \
|
||||
apk add tzdata ffmpeg bash openssl
|
||||
|
||||
ENV TZ=America/Sao_Paulo
|
||||
ENV DOCKER_ENV=true
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
@ -55,4 +57,4 @@ ENV DOCKER_ENV=true
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]
|
||||
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]
|
||||
|
@ -117,4 +117,4 @@ Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
© 2024 Evolution API
|
||||
© 2025 Evolution API
|
||||
|
@ -1,4 +1,5 @@
|
||||
services:
|
||||
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: evoapicloud/evolution-api:latest
|
||||
@ -34,19 +35,41 @@ services:
|
||||
image: postgres:15
|
||||
networks:
|
||||
- evolution-net
|
||||
command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"]
|
||||
command: [
|
||||
"postgres",
|
||||
"-c", "max_connections=200",
|
||||
"-c", "listen_addresses=*",
|
||||
"-c", "shared_buffers=256MB",
|
||||
"-c", "effective_cache_size=1GB",
|
||||
"-c", "work_mem=4MB"
|
||||
]
|
||||
restart: always
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
- POSTGRES_USER=user
|
||||
- POSTGRES_PASSWORD=pass
|
||||
- POSTGRES_DB=evolution
|
||||
- POSTGRES_DB=evolution_db
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
expose:
|
||||
- 5432
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
# pgbouncer:
|
||||
# image: edoburu/pgbouncer:latest
|
||||
# environment:
|
||||
# DB_HOST: postgres
|
||||
# DB_USER: user
|
||||
# DB_PASSWORD: pass
|
||||
# POOL_MODE: transaction
|
||||
# AUTH_TYPE: trust
|
||||
# MAX_CLIENT_CONN: 1000
|
||||
# DEFAULT_POOL_SIZE: 25
|
||||
# depends_on:
|
||||
# - postgres
|
||||
# ports:
|
||||
# - "6543:5432"
|
||||
# networks:
|
||||
# - evolution-net
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
|
3635
package-lock.json
generated
3635
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
@ -73,7 +73,7 @@
|
||||
"form-data": "^4.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18next": "^23.7.19",
|
||||
"jimp": "^0.16.13",
|
||||
"jimp": "^1.6.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@ -96,9 +96,10 @@
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"redis": "^4.7.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"sharp": "^0.32.6",
|
||||
"sharp": "^0.34.2",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tsup": "^8.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -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);
|
@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Nats` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
`events` JSON NOT NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Nats_instanceId_key` ON `Nats`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Nats` ADD CONSTRAINT `Nats_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,62 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `N8n` (
|
||||
`id` VARCHAR(191) 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` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`triggerType` ENUM('all', 'keyword', 'none') NULL,
|
||||
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
|
||||
`triggerValue` VARCHAR(191) NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `N8nSetting` (
|
||||
`id` VARCHAR(191) 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` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`n8nIdFallback` VARCHAR(100),
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `N8nSetting_instanceId_key` ON `N8nSetting`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8n` ADD CONSTRAINT `N8n_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_n8nIdFallback_fkey` FOREIGN KEY (`n8nIdFallback`) REFERENCES `N8n`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Evoai` (
|
||||
`id` VARCHAR(191) 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` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`triggerType` ENUM('all', 'keyword', 'none') NULL,
|
||||
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
|
||||
`triggerValue` VARCHAR(191) NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `EvoaiSetting` (
|
||||
`id` VARCHAR(191) 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` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`evoaiIdFallback` VARCHAR(100),
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `EvoaiSetting_instanceId_key` ON `EvoaiSetting`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Evoai` ADD CONSTRAINT `Evoai_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_evoaiIdFallback_fkey` FOREIGN KEY (`evoaiIdFallback`) REFERENCES `Evoai`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
ALTER TABLE `Media` DROP INDEX `Media_fileName_key`;
|
@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Typebot` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER DEFAULT 50;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `TypebotSetting` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER DEFAULT 50;
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `IsOnWhatsapp` ADD COLUMN `lid` VARCHAR(100);
|
@ -652,16 +652,16 @@ model N8n {
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
expire Int? @default(0) @db.Int
|
||||
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
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
@ -675,16 +675,16 @@ model N8n {
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
expire Int? @default(0) @db.Int
|
||||
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
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
@ -700,16 +700,16 @@ model Evoai {
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
expire Int? @default(0) @db.Int
|
||||
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
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
@ -723,16 +723,16 @@ model Evoai {
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
expire Int? @default(0) @db.Int
|
||||
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
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
|
@ -376,8 +376,8 @@ model TypebotSetting {
|
||||
debounceTime Int? @db.Integer
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
@ -748,4 +748,4 @@ model EvoaiSetting {
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
}
|
||||
|
752
prisma/psql_bouncer-schema.prisma
Normal file
752
prisma/psql_bouncer-schema.prisma
Normal file
@ -0,0 +1,752 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_BOUNCER_CONNECTION_URI")
|
||||
directUrl = env("DATABASE_CONNECTION_URI")
|
||||
}
|
||||
|
||||
enum InstanceConnectionStatus {
|
||||
open
|
||||
close
|
||||
connecting
|
||||
}
|
||||
|
||||
enum DeviceMessage {
|
||||
ios
|
||||
android
|
||||
web
|
||||
unknown
|
||||
desktop
|
||||
}
|
||||
|
||||
enum SessionStatus {
|
||||
opened
|
||||
closed
|
||||
paused
|
||||
}
|
||||
|
||||
enum TriggerType {
|
||||
all
|
||||
keyword
|
||||
none
|
||||
advanced
|
||||
}
|
||||
|
||||
enum TriggerOperator {
|
||||
contains
|
||||
equals
|
||||
startsWith
|
||||
endsWith
|
||||
regex
|
||||
}
|
||||
|
||||
enum OpenaiBotType {
|
||||
assistant
|
||||
chatCompletion
|
||||
}
|
||||
|
||||
enum DifyBotType {
|
||||
chatBot
|
||||
textGenerator
|
||||
agent
|
||||
workflow
|
||||
}
|
||||
|
||||
model Instance {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
connectionStatus InstanceConnectionStatus @default(open)
|
||||
ownerJid String? @db.VarChar(100)
|
||||
profileName String? @db.VarChar(100)
|
||||
profilePicUrl String? @db.VarChar(500)
|
||||
integration String? @db.VarChar(100)
|
||||
number String? @db.VarChar(100)
|
||||
businessId String? @db.VarChar(100)
|
||||
token String? @db.VarChar(255)
|
||||
clientName String? @db.VarChar(100)
|
||||
disconnectionReasonCode Int? @db.Integer
|
||||
disconnectionObject Json? @db.JsonB
|
||||
disconnectionAt DateTime? @db.Timestamp
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Chat Chat[]
|
||||
Contact Contact[]
|
||||
Message Message[]
|
||||
Webhook Webhook?
|
||||
Chatwoot Chatwoot?
|
||||
Label Label[]
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
Session Session?
|
||||
MessageUpdate MessageUpdate[]
|
||||
TypebotSetting TypebotSetting?
|
||||
Media Media[]
|
||||
OpenaiCreds OpenaiCreds[]
|
||||
OpenaiBot OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionId String @unique
|
||||
creds String? @db.Text
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
Instance Instance @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels 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
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
pushName String? @db.VarChar(100)
|
||||
profilePicUrl String? @db.VarChar(500)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@unique([remoteJid, instanceId])
|
||||
@@index([remoteJid])
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
key Json @db.JsonB
|
||||
pushName String? @db.VarChar(100)
|
||||
participant String? @db.VarChar(100)
|
||||
messageType String @db.VarChar(100)
|
||||
message Json @db.JsonB
|
||||
contextInfo Json? @db.JsonB
|
||||
source DeviceMessage
|
||||
messageTimestamp Int @db.Integer
|
||||
chatwootMessageId Int? @db.Integer
|
||||
chatwootInboxId Int? @db.Integer
|
||||
chatwootConversationId Int? @db.Integer
|
||||
chatwootContactInboxSourceId String? @db.VarChar(100)
|
||||
chatwootIsRead Boolean? @db.Boolean
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
MessageUpdate MessageUpdate[]
|
||||
Media Media?
|
||||
webhookUrl String? @db.VarChar(500)
|
||||
status String? @db.VarChar(30)
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model MessageUpdate {
|
||||
id String @id @default(cuid())
|
||||
keyId String @db.VarChar(100)
|
||||
remoteJid String @db.VarChar(100)
|
||||
fromMe Boolean @db.Boolean
|
||||
participant String? @db.VarChar(100)
|
||||
pollUpdates Json? @db.JsonB
|
||||
status String @db.VarChar(30)
|
||||
Message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
url String @db.VarChar(500)
|
||||
headers Json? @db.JsonB
|
||||
enabled Boolean? @default(true) @db.Boolean
|
||||
events Json? @db.JsonB
|
||||
webhookByEvents Boolean? @default(false) @db.Boolean
|
||||
webhookBase64 Boolean? @default(false) @db.Boolean
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Chatwoot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean? @default(true) @db.Boolean
|
||||
accountId String? @db.VarChar(100)
|
||||
token String? @db.VarChar(100)
|
||||
url String? @db.VarChar(500)
|
||||
nameInbox String? @db.VarChar(100)
|
||||
signMsg Boolean? @default(false) @db.Boolean
|
||||
signDelimiter String? @db.VarChar(100)
|
||||
number String? @db.VarChar(100)
|
||||
reopenConversation Boolean? @default(false) @db.Boolean
|
||||
conversationPending Boolean? @default(false) @db.Boolean
|
||||
mergeBrazilContacts Boolean? @default(false) @db.Boolean
|
||||
importContacts Boolean? @default(false) @db.Boolean
|
||||
importMessages Boolean? @default(false) @db.Boolean
|
||||
daysLimitImportMessages Int? @db.Integer
|
||||
organization String? @db.VarChar(100)
|
||||
logo String? @db.VarChar(500)
|
||||
ignoreJids Json?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Label {
|
||||
id String @id @default(cuid())
|
||||
labelId String? @db.VarChar(100)
|
||||
name String @db.VarChar(100)
|
||||
color String @db.VarChar(100)
|
||||
predefinedId String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@unique([labelId, instanceId])
|
||||
}
|
||||
|
||||
model Proxy {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
host String @db.VarChar(100)
|
||||
port String @db.VarChar(100)
|
||||
protocol String @db.VarChar(100)
|
||||
username String @db.VarChar(100)
|
||||
password String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Setting {
|
||||
id String @id @default(cuid())
|
||||
rejectCall Boolean @default(false) @db.Boolean
|
||||
msgCall String? @db.VarChar(100)
|
||||
groupsIgnore Boolean @default(false) @db.Boolean
|
||||
alwaysOnline Boolean @default(false) @db.Boolean
|
||||
readMessages Boolean @default(false) @db.Boolean
|
||||
readStatus Boolean @default(false) @db.Boolean
|
||||
syncFullHistory Boolean @default(false) @db.Boolean
|
||||
wavoipToken String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Rabbitmq {
|
||||
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 Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
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 Websocket {
|
||||
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 Pusher {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
appId String @db.VarChar(100)
|
||||
key String @db.VarChar(100)
|
||||
secret String @db.VarChar(100)
|
||||
cluster String @db.VarChar(100)
|
||||
useTLS 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 Typebot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
url String @db.VarChar(500)
|
||||
typebot String @db.VarChar(100)
|
||||
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
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
TypebotSetting TypebotSetting[]
|
||||
}
|
||||
|
||||
model TypebotSetting {
|
||||
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
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Date
|
||||
Message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||
messageId String @unique
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model OpenaiCreds {
|
||||
id String @id @default(cuid())
|
||||
name String? @unique @db.VarChar(255)
|
||||
apiKey String? @unique @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiAssistant OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
}
|
||||
|
||||
model OpenaiBot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType OpenaiBotType
|
||||
assistantId String? @db.VarChar(255)
|
||||
functionUrl String? @db.VarChar(500)
|
||||
model String? @db.VarChar(100)
|
||||
systemMessages Json? @db.JsonB
|
||||
assistantMessages Json? @db.JsonB
|
||||
userMessages Json? @db.JsonB
|
||||
maxTokens Int? @db.Integer
|
||||
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
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
|
||||
openaiCredsId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiSetting OpenaiSetting[]
|
||||
}
|
||||
|
||||
model IntegrationSession {
|
||||
id String @id @default(cuid())
|
||||
sessionId String @db.VarChar(255)
|
||||
remoteJid String @db.VarChar(100)
|
||||
pushName String?
|
||||
status SessionStatus
|
||||
awaitUser Boolean @default(false) @db.Boolean
|
||||
context Json?
|
||||
type String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Message Message[]
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
parameters Json? @db.JsonB
|
||||
|
||||
botId String?
|
||||
}
|
||||
|
||||
model OpenaiSetting {
|
||||
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
|
||||
speechToText Boolean? @default(false) @db.Boolean
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id])
|
||||
openaiCredsId String @unique
|
||||
Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id])
|
||||
openaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Template {
|
||||
id String @id @default(cuid())
|
||||
templateId String @unique @db.VarChar(255)
|
||||
name String @unique @db.VarChar(255)
|
||||
template Json @db.JsonB
|
||||
webhookUrl String? @db.VarChar(500)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model Dify {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType DifyBotType
|
||||
apiUrl 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
|
||||
DifySetting DifySetting[]
|
||||
}
|
||||
|
||||
model DifySetting {
|
||||
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 Dify? @relation(fields: [difyIdFallback], references: [id])
|
||||
difyIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model EvolutionBot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
apiUrl 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
|
||||
EvolutionBotSetting EvolutionBotSetting[]
|
||||
}
|
||||
|
||||
model EvolutionBotSetting {
|
||||
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 EvolutionBot? @relation(fields: [botIdFallback], references: [id])
|
||||
botIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Flowise {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
apiUrl 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
|
||||
FlowiseSetting FlowiseSetting[]
|
||||
}
|
||||
|
||||
model FlowiseSetting {
|
||||
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 Flowise? @relation(fields: [flowiseIdFallback], references: [id])
|
||||
flowiseIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model IsOnWhatsapp {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @unique @db.VarChar(100)
|
||||
jidOptions String
|
||||
lid String? @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
@ -11,11 +11,28 @@ if (!DATABASE_PROVIDER) {
|
||||
console.warn(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
|
||||
}
|
||||
|
||||
// Função para determinar qual pasta de migrations usar
|
||||
// Função para determinar qual pasta de migrations usar
|
||||
function getMigrationsFolder(provider) {
|
||||
switch (provider) {
|
||||
case 'psql_bouncer':
|
||||
return 'postgresql-migrations'; // psql_bouncer usa as migrations do postgresql
|
||||
default:
|
||||
return `${provider}-migrations`;
|
||||
}
|
||||
}
|
||||
|
||||
const migrationsFolder = getMigrationsFolder(databaseProviderDefault);
|
||||
|
||||
let command = process.argv
|
||||
.slice(2)
|
||||
.join(' ')
|
||||
.replace(/DATABASE_PROVIDER/g, databaseProviderDefault);
|
||||
|
||||
// Substituir referências à pasta de migrations pela pasta correta
|
||||
const migrationsPattern = new RegExp(`${databaseProviderDefault}-migrations`, 'g');
|
||||
command = command.replace(migrationsPattern, migrationsFolder);
|
||||
|
||||
if (command.includes('rmdir') && existsSync('prisma\\migrations')) {
|
||||
try {
|
||||
execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' });
|
||||
@ -32,4 +49,4 @@ try {
|
||||
} catch (error) {
|
||||
console.error(`Error executing command: ${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
@ -455,39 +455,46 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
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;
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||
|
||||
let mediaType: string;
|
||||
let mimetype = audioFile?.mimetype || file.mimetype;
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
const fileBuffer = audioFile?.buffer || file?.buffer;
|
||||
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
@ -429,107 +429,114 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
try {
|
||||
const message: any = received;
|
||||
|
||||
const id = message.messages[0][message.messages[0].type].id;
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${id}`;
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.get(urlServer, { headers });
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||
|
||||
const buffer = await axios.get(result.data.url, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
let mediaType;
|
||||
|
||||
if (message.messages[0].document) {
|
||||
mediaType = 'document';
|
||||
} else if (message.messages[0].image) {
|
||||
mediaType = 'image';
|
||||
} else if (message.messages[0].audio) {
|
||||
mediaType = 'audio';
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
mediaType = 'video';
|
||||
}
|
||||
const id = message.messages[0][message.messages[0].type].id;
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${id}`;
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.get(urlServer, { headers });
|
||||
|
||||
const mimetype = result.data?.mime_type || result.headers['content-type'];
|
||||
const buffer = await axios.get(result.data.url, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const contentDisposition = result.headers['content-disposition'];
|
||||
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="(.+?)"/);
|
||||
if (match) {
|
||||
fileName = match[1];
|
||||
let mediaType;
|
||||
|
||||
if (message.messages[0].document) {
|
||||
mediaType = 'document';
|
||||
} else if (message.messages[0].image) {
|
||||
mediaType = 'image';
|
||||
} else if (message.messages[0].audio) {
|
||||
mediaType = 'audio';
|
||||
} else {
|
||||
mediaType = 'video';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 mimetype = result.data?.mime_type || result.headers['content-type'];
|
||||
|
||||
const contentDisposition = result.headers['content-disposition'];
|
||||
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="(.+?)"/);
|
||||
if (match) {
|
||||
fileName = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const size = result.headers['content-length'] || buffer.data.byteLength;
|
||||
// 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 fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
||||
const size = result.headers['content-length'] || buffer.data.byteLength;
|
||||
|
||||
await s3Service.uploadFile(fullName, buffer.data, size, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
||||
|
||||
const createdMessage = await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
await s3Service.uploadFile(fullName, buffer.data, size, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: createdMessage.id,
|
||||
instanceId: this.instanceId,
|
||||
type: mediaType,
|
||||
fileName: fullName,
|
||||
mimetype,
|
||||
},
|
||||
});
|
||||
const createdMessage = await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
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: {
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: createdMessage.id,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
type: mediaType,
|
||||
fileName: fullName,
|
||||
mimetype,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
openAiDefaultSettings &&
|
||||
openAiDefaultSettings.openaiCredsId &&
|
||||
openAiDefaultSettings.speechToText
|
||||
) {
|
||||
try {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
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 (speechError) {
|
||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,7 @@ import makeWASocket, {
|
||||
Contact,
|
||||
delay,
|
||||
DisconnectReason,
|
||||
downloadContentFromMessage,
|
||||
downloadMediaMessage,
|
||||
generateWAMessageFromContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
@ -122,7 +123,7 @@ import makeWASocket, {
|
||||
WABrowserDescription,
|
||||
WAMediaUpload,
|
||||
WAMessage,
|
||||
WAMessageUpdate,
|
||||
WAMessageKey,
|
||||
WAPresence,
|
||||
WASocket,
|
||||
} from 'baileys';
|
||||
@ -894,7 +895,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}: {
|
||||
chats: Chat[];
|
||||
contacts: Contact[];
|
||||
messages: proto.IWebMessageInfo[];
|
||||
messages: WAMessage[];
|
||||
isLatest?: boolean;
|
||||
progress?: number;
|
||||
syncType?: proto.HistorySync.HistorySyncType;
|
||||
@ -980,6 +981,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.key.remoteJid?.includes('@lid') && m.key.senderPn) {
|
||||
m.key.remoteJid = m.key.senderPn;
|
||||
}
|
||||
|
||||
if (Long.isLong(m?.messageTimestamp)) {
|
||||
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
||||
}
|
||||
@ -1037,11 +1042,29 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
},
|
||||
|
||||
'messages.upsert': async (
|
||||
{ messages, type, requestId }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string },
|
||||
{ messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string },
|
||||
settings: any,
|
||||
) => {
|
||||
try {
|
||||
for (const received of messages) {
|
||||
if (received.key.remoteJid?.includes('@lid') && received.key.senderPn) {
|
||||
(received.key as { previousRemoteJid?: string | null }).previousRemoteJid = received.key.remoteJid;
|
||||
received.key.remoteJid = received.key.senderPn;
|
||||
}
|
||||
if (
|
||||
received?.messageStubParameters?.some?.((param) =>
|
||||
[
|
||||
'No matching sessions found for message',
|
||||
'Bad MAC',
|
||||
'failed to decrypt message',
|
||||
'SessionError',
|
||||
'Invalid PreKey ID',
|
||||
].some((err) => param?.includes?.(err)),
|
||||
)
|
||||
) {
|
||||
this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`);
|
||||
continue;
|
||||
}
|
||||
if (received.message?.conversation || received.message?.extendedTextMessage?.text) {
|
||||
const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
|
||||
|
||||
@ -1233,33 +1256,41 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (this.configService.get<S3>('S3').ENABLE) {
|
||||
try {
|
||||
const message: any = received;
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
const fullName = join(
|
||||
`${this.instance.id}`,
|
||||
received.key.remoteJid,
|
||||
mediaType,
|
||||
`${Date.now()}_${fileName}`,
|
||||
);
|
||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(message);
|
||||
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: msg.id,
|
||||
instanceId: this.instanceId,
|
||||
type: mediaType,
|
||||
fileName: fullName,
|
||||
mimetype,
|
||||
},
|
||||
});
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
const fullName = join(
|
||||
`${this.instance.id}`,
|
||||
received.key.remoteJid,
|
||||
mediaType,
|
||||
`${Date.now()}_${fileName}`,
|
||||
);
|
||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: msg.id,
|
||||
instanceId: this.instanceId,
|
||||
type: mediaType,
|
||||
fileName: fullName,
|
||||
mimetype,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
|
||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
@ -1363,7 +1394,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
},
|
||||
|
||||
'messages.update': async (args: WAMessageUpdate[], settings: any) => {
|
||||
'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
|
||||
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
||||
|
||||
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
||||
@ -1373,6 +1404,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.remoteJid?.includes('@lid') && key.senderPn) {
|
||||
key.remoteJid = key.senderPn;
|
||||
}
|
||||
|
||||
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
||||
|
||||
const cached = await this.baileysCache.get(updateKey);
|
||||
@ -2130,31 +2165,39 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
if (isMedia && this.configService.get<S3>('S3').ENABLE) {
|
||||
try {
|
||||
const message: any = messageRaw;
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(message);
|
||||
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||
|
||||
const fullName = join(
|
||||
`${this.instance.id}`,
|
||||
messageRaw.key.remoteJid,
|
||||
`${messageRaw.key.id}`,
|
||||
mediaType,
|
||||
fileName,
|
||||
);
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
|
||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
|
||||
await this.prismaRepository.media.create({
|
||||
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
|
||||
});
|
||||
const fullName = join(
|
||||
`${this.instance.id}`,
|
||||
messageRaw.key.remoteJid,
|
||||
`${messageRaw.key.id}`,
|
||||
mediaType,
|
||||
fileName,
|
||||
);
|
||||
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
await this.prismaRepository.media.create({
|
||||
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
|
||||
});
|
||||
|
||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
|
||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
@ -3422,6 +3465,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
public async mapMediaType(mediaType) {
|
||||
const map = {
|
||||
imageMessage: 'image',
|
||||
videoMessage: 'video',
|
||||
documentMessage: 'document',
|
||||
stickerMessage: 'sticker',
|
||||
audioMessage: 'audio',
|
||||
ptvMessage: 'video',
|
||||
};
|
||||
return map[mediaType] || null;
|
||||
}
|
||||
|
||||
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
|
||||
try {
|
||||
const m = data?.message;
|
||||
@ -3446,28 +3501,76 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
let mediaMessage: any;
|
||||
let mediaType: string;
|
||||
|
||||
for (const type of TypeMediaMessage) {
|
||||
mediaMessage = msg.message[type];
|
||||
if (mediaMessage) {
|
||||
mediaType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (msg.message?.templateMessage) {
|
||||
const template =
|
||||
msg.message.templateMessage.hydratedTemplate || msg.message.templateMessage.hydratedFourRowTemplate;
|
||||
|
||||
if (!mediaMessage) {
|
||||
throw 'The message is not of the media type';
|
||||
for (const type of TypeMediaMessage) {
|
||||
if (template[type]) {
|
||||
mediaMessage = template[type];
|
||||
mediaType = type;
|
||||
msg.message = { [type]: { ...template[type], url: template[type].staticUrl } };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mediaMessage) {
|
||||
throw 'Template message does not contain a supported media type';
|
||||
}
|
||||
} else {
|
||||
for (const type of TypeMediaMessage) {
|
||||
mediaMessage = msg.message[type];
|
||||
if (mediaMessage) {
|
||||
mediaType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mediaMessage) {
|
||||
throw 'The message is not of the media type';
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof mediaMessage['mediaKey'] === 'object') {
|
||||
msg.message = JSON.parse(JSON.stringify(msg.message));
|
||||
}
|
||||
|
||||
const buffer = await downloadMediaMessage(
|
||||
{ key: msg?.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
|
||||
);
|
||||
let buffer: Buffer;
|
||||
|
||||
try {
|
||||
buffer = await downloadMediaMessage(
|
||||
{ key: msg?.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error('Download Media failed, trying to retry in 5 seconds...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
|
||||
if (!mediaType) throw new Error('Could not determine mediaType for fallback');
|
||||
|
||||
try {
|
||||
const media = await downloadContentFromMessage(
|
||||
{
|
||||
mediaKey: msg.message?.[mediaType]?.mediaKey,
|
||||
directPath: msg.message?.[mediaType]?.directPath,
|
||||
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
|
||||
},
|
||||
await this.mapMediaType(mediaType),
|
||||
{},
|
||||
);
|
||||
const chunks = [];
|
||||
for await (const chunk of media) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
buffer = Buffer.concat(chunks);
|
||||
this.logger.info('Download Media with downloadContentFromMessage was successful!');
|
||||
} catch (fallbackErr) {
|
||||
this.logger.error('Download Media with downloadContentFromMessage also failed!');
|
||||
throw fallbackErr;
|
||||
}
|
||||
}
|
||||
const typeMessage = getContentType(msg.message);
|
||||
|
||||
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);
|
||||
|
@ -26,7 +26,7 @@ import axios from 'axios';
|
||||
import { proto } from 'baileys';
|
||||
import dayjs from 'dayjs';
|
||||
import FormData from 'form-data';
|
||||
import Jimp from 'jimp';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
import Long from 'long';
|
||||
import mimeTypes from 'mime-types';
|
||||
import path from 'path';
|
||||
@ -457,6 +457,24 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeContacts(baseId: number, mergeId: number) {
|
||||
try {
|
||||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||||
method: 'POST',
|
||||
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
|
||||
body: {
|
||||
base_contact_id: baseId,
|
||||
mergee_contact_id: mergeId,
|
||||
},
|
||||
});
|
||||
|
||||
return contact;
|
||||
} catch {
|
||||
this.logger.error('Error merging contacts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeBrazilianContacts(contacts: any[]) {
|
||||
try {
|
||||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||||
@ -549,24 +567,34 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
|
||||
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
|
||||
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
|
||||
const remoteJid = body.key.remoteJid;
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||
const maxWaitTime = 5000; // 5 secounds
|
||||
|
||||
try {
|
||||
// Processa atualização de contatos já criados @lid
|
||||
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) {
|
||||
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
|
||||
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})`,
|
||||
`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, {
|
||||
const updateContact = await this.updateContact(instance, contact.id, {
|
||||
identifier: body.key.senderPn,
|
||||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
||||
});
|
||||
|
||||
if (updateContact === null) {
|
||||
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
|
||||
if (baseContact) {
|
||||
await this.mergeContacts(baseContact.id, contact.id);
|
||||
this.logger.verbose(
|
||||
`Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger.verbose(`--- Start createConversation ---`);
|
||||
@ -685,7 +713,6 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
chatId,
|
||||
@ -693,7 +720,7 @@ export class ChatwootService {
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
jid,
|
||||
remoteJid,
|
||||
);
|
||||
}
|
||||
|
||||
@ -2101,9 +2128,11 @@ export class ChatwootService {
|
||||
const fileData = Buffer.from(imgBuffer.data, 'binary');
|
||||
|
||||
const img = await Jimp.read(fileData);
|
||||
await img.cover(320, 180);
|
||||
|
||||
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG);
|
||||
await img.cover({
|
||||
w: 320,
|
||||
h: 180,
|
||||
});
|
||||
const processedBuffer = await img.getBuffer(JimpMime.png);
|
||||
|
||||
const fileStream = new Readable();
|
||||
fileStream._read = () => {}; // _read is required but you can noop it
|
||||
|
@ -70,7 +70,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
||||
}
|
||||
|
||||
const callId = `req-${uuidv4().substring(0, 8)}`;
|
||||
const messageId = msg?.key?.id || uuidv4();
|
||||
const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
|
||||
|
||||
// Prepare message parts
|
||||
const parts = [
|
||||
|
@ -119,7 +119,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
id: instance.instanceId,
|
||||
name: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
@ -290,7 +290,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
|
@ -186,7 +186,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
messages,
|
||||
input,
|
||||
clientSideActions,
|
||||
this.applyFormatting,
|
||||
this.applyFormatting.bind(this),
|
||||
this.prismaRepository,
|
||||
).catch((err) => {
|
||||
console.error('Erro ao processar mensagens:', err);
|
||||
|
@ -8,7 +8,12 @@ import { EmitData, EventController, EventControllerInterface } from '../event.co
|
||||
|
||||
export class RabbitmqController extends EventController implements EventControllerInterface {
|
||||
public amqpChannel: amqp.Channel | null = null;
|
||||
private amqpConnection: amqp.Connection | null = null;
|
||||
private readonly logger = new Logger('RabbitmqController');
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 10;
|
||||
private reconnectDelay = 5000; // 5 seconds
|
||||
private isReconnecting = false;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor, configService.get<Rabbitmq>('RABBITMQ')?.ENABLED, 'rabbitmq');
|
||||
@ -19,7 +24,11 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
|
||||
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
|
||||
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
|
||||
@ -33,22 +42,61 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
password: url.password || 'guest',
|
||||
vhost: url.pathname.slice(1) || '/',
|
||||
frameMax: frameMax,
|
||||
heartbeat: 30, // Add heartbeat of 30 seconds
|
||||
};
|
||||
|
||||
amqp.connect(connectionOptions, (error, connection) => {
|
||||
if (error) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.connect',
|
||||
message: 'Failed to connect to RabbitMQ',
|
||||
error: error.message || error,
|
||||
});
|
||||
reject(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection event handlers
|
||||
connection.on('error', (err) => {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.connectionError',
|
||||
message: 'RabbitMQ connection error',
|
||||
error: err.message || err,
|
||||
});
|
||||
this.handleConnectionLoss();
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
this.logger.warn('RabbitMQ connection closed');
|
||||
this.handleConnectionLoss();
|
||||
});
|
||||
|
||||
connection.createChannel((channelError, channel) => {
|
||||
if (channelError) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.createChannel',
|
||||
message: 'Failed to create RabbitMQ channel',
|
||||
error: channelError.message || channelError,
|
||||
});
|
||||
reject(channelError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Channel event handlers
|
||||
channel.on('error', (err) => {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.channelError',
|
||||
message: 'RabbitMQ channel error',
|
||||
error: err.message || err,
|
||||
});
|
||||
this.handleConnectionLoss();
|
||||
});
|
||||
|
||||
channel.on('close', () => {
|
||||
this.logger.warn('RabbitMQ channel closed');
|
||||
this.handleConnectionLoss();
|
||||
});
|
||||
|
||||
const exchangeName = rabbitmqExchangeName;
|
||||
|
||||
channel.assertExchange(exchangeName, 'topic', {
|
||||
@ -56,16 +104,81 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
autoDelete: false,
|
||||
});
|
||||
|
||||
this.amqpConnection = connection;
|
||||
this.amqpChannel = channel;
|
||||
this.reconnectAttempts = 0; // Reset reconnect attempts on successful connection
|
||||
this.isReconnecting = false;
|
||||
|
||||
this.logger.info('AMQP initialized');
|
||||
this.logger.info('AMQP initialized successfully');
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) this.initGlobalQueues();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) {
|
||||
this.initGlobalQueues();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.init',
|
||||
message: 'Failed to initialize AMQP',
|
||||
error: error.message || error,
|
||||
});
|
||||
this.scheduleReconnect();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
private handleConnectionLoss(): void {
|
||||
if (this.isReconnecting) {
|
||||
return; // Already attempting to reconnect
|
||||
}
|
||||
|
||||
this.amqpChannel = null;
|
||||
this.amqpConnection = null;
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.logger.error(
|
||||
`Maximum reconnect attempts (${this.maxReconnectAttempts}) reached. Stopping reconnection attempts.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isReconnecting) {
|
||||
return; // Already scheduled
|
||||
}
|
||||
|
||||
this.isReconnecting = true;
|
||||
this.reconnectAttempts++;
|
||||
|
||||
const delay = this.reconnectDelay * Math.pow(2, Math.min(this.reconnectAttempts - 1, 5)); // Exponential backoff with max delay
|
||||
|
||||
this.logger.info(
|
||||
`Scheduling RabbitMQ reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`,
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
this.logger.info(
|
||||
`Attempting to reconnect to RabbitMQ (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
|
||||
);
|
||||
await this.connect();
|
||||
this.logger.info('Successfully reconnected to RabbitMQ');
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.scheduleReconnect',
|
||||
message: `Reconnection attempt ${this.reconnectAttempts} failed`,
|
||||
error: error.message || error,
|
||||
});
|
||||
this.isReconnecting = false;
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private set channel(channel: amqp.Channel) {
|
||||
@ -76,6 +189,17 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
return this.amqpChannel;
|
||||
}
|
||||
|
||||
private async ensureConnection(): Promise<boolean> {
|
||||
if (!this.amqpChannel) {
|
||||
this.logger.warn('AMQP channel is not available, attempting to reconnect...');
|
||||
if (!this.isReconnecting) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instanceName,
|
||||
origin,
|
||||
@ -95,6 +219,11 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.ensureConnection())) {
|
||||
this.logger.warn(`Failed to emit event ${event} for instance ${instanceName}: No AMQP connection`);
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceRabbitmq = await this.get(instanceName);
|
||||
const rabbitmqLocal = instanceRabbitmq?.events;
|
||||
const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
|
||||
@ -154,7 +283,15 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.emit',
|
||||
message: `Error publishing local RabbitMQ message (attempt ${retry + 1}/3)`,
|
||||
error: error.message || error,
|
||||
});
|
||||
retry++;
|
||||
if (retry >= 3) {
|
||||
this.handleConnectionLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,7 +336,15 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.emit',
|
||||
message: `Error publishing global RabbitMQ message (attempt ${retry + 1}/3)`,
|
||||
error: error.message || error,
|
||||
});
|
||||
retry++;
|
||||
if (retry >= 3) {
|
||||
this.handleConnectionLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,41 +353,57 @@ export class RabbitmqController extends EventController implements EventControll
|
||||
private async initGlobalQueues(): Promise<void> {
|
||||
this.logger.info('Initializing global queues');
|
||||
|
||||
if (!(await this.ensureConnection())) {
|
||||
this.logger.error('Cannot initialize global queues: No AMQP connection');
|
||||
return;
|
||||
}
|
||||
|
||||
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
|
||||
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
|
||||
const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
|
||||
|
||||
if (!events) {
|
||||
this.logger.warn('No events to initialize on AMQP');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const eventKeys = Object.keys(events);
|
||||
|
||||
eventKeys.forEach((event) => {
|
||||
if (events[event] === false) return;
|
||||
for (const event of eventKeys) {
|
||||
if (events[event] === false) continue;
|
||||
|
||||
const queueName =
|
||||
prefixKey !== ''
|
||||
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
|
||||
: `${event.replace(/_/g, '.').toLowerCase()}`;
|
||||
const exchangeName = rabbitmqExchangeName;
|
||||
try {
|
||||
const queueName =
|
||||
prefixKey !== ''
|
||||
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
|
||||
: `${event.replace(/_/g, '.').toLowerCase()}`;
|
||||
const exchangeName = rabbitmqExchangeName;
|
||||
|
||||
this.amqpChannel.assertExchange(exchangeName, 'topic', {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
await this.amqpChannel.assertExchange(exchangeName, 'topic', {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
|
||||
this.amqpChannel.assertQueue(queueName, {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
arguments: {
|
||||
'x-queue-type': 'quorum',
|
||||
},
|
||||
});
|
||||
await this.amqpChannel.assertQueue(queueName, {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
arguments: {
|
||||
'x-queue-type': 'quorum',
|
||||
},
|
||||
});
|
||||
|
||||
this.amqpChannel.bindQueue(queueName, exchangeName, event);
|
||||
});
|
||||
await this.amqpChannel.bindQueue(queueName, exchangeName, event);
|
||||
|
||||
this.logger.info(`Global queue initialized: ${queueName}`);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
local: 'RabbitmqController.initGlobalQueues',
|
||||
message: `Failed to initialize global queue for event ${event}`,
|
||||
error: error.message || error,
|
||||
});
|
||||
this.handleConnectionLoss();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -738,6 +738,7 @@ export class ChannelStartupService {
|
||||
"Chat"."name" as "pushName",
|
||||
"Chat"."createdAt" as "windowStart",
|
||||
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
|
||||
"Chat"."unreadMessages" as "unreadMessages",
|
||||
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
|
||||
"Message"."id" AS lastMessageId,
|
||||
"Message"."key" AS lastMessage_key,
|
||||
@ -797,7 +798,7 @@ export class ChannelStartupService {
|
||||
windowExpires: contact.windowexpires,
|
||||
windowActive: contact.windowactive,
|
||||
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
|
||||
unreadCount: 0,
|
||||
unreadCount: contact.unreadMessages,
|
||||
isSaved: !!contact.contactid,
|
||||
};
|
||||
});
|
||||
@ -813,4 +814,28 @@ export class ChannelStartupService {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public hasValidMediaContent(message: any): boolean {
|
||||
if (!message?.message) return false;
|
||||
|
||||
const msg = message.message;
|
||||
|
||||
// Se só tem messageContextInfo, não é mídia válida
|
||||
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verifica se tem pelo menos um tipo de mídia válido
|
||||
const mediaTypes = [
|
||||
'imageMessage',
|
||||
'videoMessage',
|
||||
'stickerMessage',
|
||||
'documentMessage',
|
||||
'documentWithCaptionMessage',
|
||||
'ptvMessage',
|
||||
'audioMessage',
|
||||
];
|
||||
|
||||
return mediaTypes.some((type) => msg[type] && Object.keys(msg[type]).length > 0);
|
||||
}
|
||||
}
|
||||
|
19
src/railway.json
Normal file
19
src/railway.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://railway.com/railway.schema.json",
|
||||
"build": {
|
||||
"builder": "DOCKERFILE",
|
||||
"dockerfilePath": "Dockerfile"
|
||||
},
|
||||
"deploy": {
|
||||
"runtime": "V2",
|
||||
"numReplicas": 1,
|
||||
"sleepApplication": false,
|
||||
"multiRegionConfig": {
|
||||
"us-east4-eqdc4a": {
|
||||
"numReplicas": 1
|
||||
}
|
||||
},
|
||||
"restartPolicyType": "ON_FAILURE",
|
||||
"restartPolicyMaxRetries": 10
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user