Merge remote-tracking branch 'upstream/v2.0.0' into v2.0.0

This commit is contained in:
Diego Marino 2024-08-22 21:33:55 -03:00
commit 9099adf363
58 changed files with 8335 additions and 5905 deletions

View File

@ -135,6 +135,10 @@ CONFIG_SESSION_PHONE_CLIENT=Evolution API
# Browser Name = Chrome | Firefox | Edge | Opera | Safari # Browser Name = Chrome | Firefox | Edge | Opera | Safari
CONFIG_SESSION_PHONE_NAME=Chrome 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.1015901307
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30
# Color of the QRCode on base64 # Color of the QRCode on base64
@ -151,6 +155,8 @@ CHATWOOT_ENABLED=false
CHATWOOT_MESSAGE_READ=true CHATWOOT_MESSAGE_READ=true
# If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp. # If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp.
CHATWOOT_MESSAGE_DELETE=true CHATWOOT_MESSAGE_DELETE=true
# If you leave this option as true, a contact will be created on Chatwoot to provide the QR Code and update messages about the instance.
CHATWOOT_BOT_CONTACT=true
# This db connection is used to import messages from whatsapp to chatwoot database # This db connection is used to import messages from whatsapp to chatwoot database
CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgresql://user:passwprd@host:5432/chatwoot?sslmode=disable CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgresql://user:passwprd@host:5432/chatwoot?sslmode=disable
CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=true CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=true

View File

@ -4,10 +4,15 @@
* Improved layout manager * Improved layout manager
* Translation in manager: English, Portuguese, Spanish and French * Translation in manager: English, Portuguese, Spanish and French
* Generic Bot Integration
* Option to disable chatwoot bot contact with CHATWOOT_BOT_CONTACT
* Added flowise integration
* Added evolution channel on instance create
### Fixed ### Fixed
* Refactor websocket structure * Refactor integrations structure for modular system
* Fixed dify agent integration
# 2.0.10 (2024-08-16 16:23) # 2.0.10 (2024-08-16 16:23)

View File

@ -2,7 +2,7 @@ version: "3.7"
services: services:
evolution_v2: evolution_v2:
image: atendai/evolution-api:v2.0.9 image: atendai/evolution-api:v2.0.10
volumes: volumes:
- evolution_instances:/evolution/instances - evolution_instances:/evolution/instances
networks: networks:
@ -92,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.2413.51 - 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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
<link rel="icon" type="image/png" href="/assets/images/evolution-logo.png" /> <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-CNPbB3iJ.js"></script> <script type="module" crossorigin src="/assets/index-BmAfUzu7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJ9JMAl_.css"> <link rel="stylesheet" crossorigin href="/assets/index-BJ9JMAl_.css">
</head> </head>
<body> <body>

View File

@ -169,17 +169,5 @@ ALTER TABLE `Websocket`
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL; MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- Remove the duplicates
DELETE c1
FROM `Contact` c1
INNER JOIN (
SELECT MIN(id) as id
FROM `Contact`
GROUP BY
`remoteJid`, `instanceId`
) c2 ON c1.`remoteJid` = c2.`remoteJid`
AND c1.`instanceId` = c2.`instanceId`
AND c1.id != c2.id;
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX `Contact_remoteJid_instanceId_key` ON `Contact` (`remoteJid`, `instanceId`); CREATE UNIQUE INDEX `Contact_remoteJid_instanceId_key` ON `Contact` (`remoteJid`, `instanceId`);

View File

@ -0,0 +1,269 @@
/*
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 `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 drop the column `difyId` on the `IntegrationSession` table. All the data in the column will be lost.
- You are about to drop the column `openaiBotId` on the `IntegrationSession` table. All the data in the column will be lost.
- You are about to drop the column `typebotId` on the `IntegrationSession` table. All the data in the column will be lost.
- 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 `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`.
*/
-- DropForeignKey
ALTER TABLE `IntegrationSession` DROP FOREIGN KEY `IntegrationSession_difyId_fkey`;
-- DropForeignKey
ALTER TABLE `IntegrationSession` DROP FOREIGN KEY `IntegrationSession_openaiBotId_fkey`;
-- DropForeignKey
ALTER TABLE `IntegrationSession` DROP FOREIGN KEY `IntegrationSession_typebotId_fkey`;
-- DropIndex
DROP INDEX `Message_typebotSessionId_fkey` ON `Message`;
-- 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 `triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
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 `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `IntegrationSession` DROP COLUMN `difyId`,
DROP COLUMN `openaiBotId`,
DROP COLUMN `typebotId`,
ADD COLUMN `botId` VARCHAR(191) NULL,
ADD COLUMN `context` JSON NULL,
MODIFY `createdAt` TIMESTAMP 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 `triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
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 `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,
MODIFY `triggerType` ENUM('all', 'keyword', 'none', 'advanced') 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 `GenericBot` (
`id` VARCHAR(191) NOT NULL,
`enabled` BOOLEAN NOT NULL DEFAULT true,
`description` VARCHAR(255) NULL,
`apiUrl` VARCHAR(255) NULL,
`apiKey` VARCHAR(255) NULL,
`expire` INTEGER NULL DEFAULT 0,
`keywordFinish` VARCHAR(100) NULL,
`delayMessage` INTEGER NULL,
`unknownMessage` VARCHAR(100) NULL,
`listeningFromMe` BOOLEAN NULL DEFAULT false,
`stopBotFromMe` BOOLEAN NULL DEFAULT false,
`keepOpen` BOOLEAN NULL DEFAULT false,
`debounceTime` INTEGER NULL,
`ignoreJids` JSON NULL,
`triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
`triggerValue` VARCHAR(191) NULL,
`createdAt` TIMESTAMP NULL 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 `GenericSetting` (
`id` VARCHAR(191) NOT NULL,
`expire` INTEGER NULL DEFAULT 0,
`keywordFinish` VARCHAR(100) NULL,
`delayMessage` INTEGER NULL,
`unknownMessage` VARCHAR(100) NULL,
`listeningFromMe` BOOLEAN NULL DEFAULT false,
`stopBotFromMe` BOOLEAN NULL DEFAULT false,
`keepOpen` BOOLEAN NULL DEFAULT false,
`debounceTime` INTEGER NULL,
`ignoreJids` JSON NULL,
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP NOT NULL,
`botIdFallback` VARCHAR(100) NULL,
`instanceId` VARCHAR(191) NOT NULL,
UNIQUE INDEX `GenericSetting_instanceId_key`(`instanceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Flowise` (
`id` VARCHAR(191) NOT NULL,
`enabled` BOOLEAN NOT NULL DEFAULT true,
`description` VARCHAR(255) NULL,
`apiUrl` VARCHAR(255) NULL,
`apiKey` VARCHAR(255) NULL,
`expire` INTEGER NULL DEFAULT 0,
`keywordFinish` VARCHAR(100) NULL,
`delayMessage` INTEGER NULL,
`unknownMessage` VARCHAR(100) NULL,
`listeningFromMe` BOOLEAN NULL DEFAULT false,
`stopBotFromMe` BOOLEAN NULL DEFAULT false,
`keepOpen` BOOLEAN NULL DEFAULT false,
`debounceTime` INTEGER NULL,
`ignoreJids` JSON NULL,
`triggerType` ENUM('all', 'keyword', 'none', 'advanced') NULL,
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
`triggerValue` VARCHAR(191) NULL,
`createdAt` TIMESTAMP NULL 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 `FlowiseSetting` (
`id` VARCHAR(191) NOT NULL,
`expire` INTEGER NULL DEFAULT 0,
`keywordFinish` VARCHAR(100) NULL,
`delayMessage` INTEGER NULL,
`unknownMessage` VARCHAR(100) NULL,
`listeningFromMe` BOOLEAN NULL DEFAULT false,
`stopBotFromMe` BOOLEAN NULL DEFAULT false,
`keepOpen` BOOLEAN NULL DEFAULT false,
`debounceTime` INTEGER NULL,
`ignoreJids` JSON NULL,
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP NOT NULL,
`flowiseIdFallback` VARCHAR(100) NULL,
`instanceId` VARCHAR(191) NOT NULL,
UNIQUE INDEX `FlowiseSetting_instanceId_key`(`instanceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `GenericBot` ADD CONSTRAINT `GenericBot_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `GenericSetting` ADD CONSTRAINT `GenericSetting_botIdFallback_fkey` FOREIGN KEY (`botIdFallback`) REFERENCES `GenericBot`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `GenericSetting` ADD CONSTRAINT `GenericSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Flowise` ADD CONSTRAINT `Flowise_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `FlowiseSetting` ADD CONSTRAINT `FlowiseSetting_flowiseIdFallback_fkey` FOREIGN KEY (`flowiseIdFallback`) REFERENCES `Flowise`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `FlowiseSetting` ADD CONSTRAINT `FlowiseSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -100,6 +100,10 @@ model Instance {
Dify Dify[] Dify Dify[]
DifySetting DifySetting? DifySetting DifySetting?
integrationSessions IntegrationSession[] integrationSessions IntegrationSession[]
GenericBot GenericBot[]
GenericSetting GenericSetting?
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
} }
model Session { model Session {
@ -284,29 +288,28 @@ model Websocket {
} }
model Typebot { model Typebot {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) enabled Boolean @default(true)
description String? @db.VarChar(255) description String? @db.VarChar(255)
url String @db.VarChar(500) url String @db.VarChar(500)
typebot String @db.VarChar(100) typebot String @db.VarChar(100)
expire Int? @default(0) @db.Int expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) keepOpen Boolean? @default(false)
debounceTime Int? @db.Int debounceTime Int? @db.Int
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
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
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[]
sessions IntegrationSession[]
} }
model TypebotSetting { model TypebotSetting {
@ -335,6 +338,7 @@ model IntegrationSession {
pushName String? pushName String?
status SessionStatus status SessionStatus
awaitUser Boolean @default(false) awaitUser Boolean @default(false)
context 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
Message Message[] Message Message[]
@ -342,14 +346,7 @@ model IntegrationSession {
instanceId String instanceId String
parameters Json? parameters Json?
OpenaiBot OpenaiBot? @relation(fields: [openaiBotId], references: [id], onDelete: Cascade) botId String?
openaiBotId String?
DifyBot Dify? @relation(fields: [difyId], references: [id], onDelete: Cascade)
difyId String?
Typebot Typebot? @relation(fields: [typebotId], references: [id], onDelete: Cascade)
typebotId String?
} }
model Media { model Media {
@ -377,37 +374,36 @@ model OpenaiCreds {
} }
model OpenaiBot { model OpenaiBot {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) enabled Boolean @default(true)
description String? @db.VarChar(255) description String? @db.VarChar(255)
botType OpenaiBotType botType OpenaiBotType
assistantId String? @db.VarChar(255) assistantId String? @db.VarChar(255)
functionUrl String? @db.VarChar(500) functionUrl String? @db.VarChar(500)
model String? @db.VarChar(100) model String? @db.VarChar(100)
systemMessages Json? @db.Json systemMessages Json? @db.Json
assistantMessages Json? @db.Json assistantMessages Json? @db.Json
userMessages Json? @db.Json userMessages Json? @db.Json
maxTokens Int? @db.Int maxTokens Int? @db.Int
expire Int? @default(0) @db.Int expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) keepOpen Boolean? @default(false)
debounceTime Int? @db.Int debounceTime Int? @db.Int
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
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
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade) OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
openaiCredsId String openaiCredsId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
OpenaiSetting OpenaiSetting[] OpenaiSetting OpenaiSetting[]
sessions IntegrationSession[]
} }
model OpenaiSetting { model OpenaiSetting {
@ -445,30 +441,29 @@ model Template {
} }
model Dify { model Dify {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) enabled Boolean @default(true)
description String? @db.VarChar(255) description String? @db.VarChar(255)
botType DifyBotType botType DifyBotType
apiUrl String? @db.VarChar(255) apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255) apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Int expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) keepOpen Boolean? @default(false)
debounceTime Int? @db.Int debounceTime Int? @db.Int
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
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
DifySetting DifySetting[] DifySetting DifySetting[]
sessions IntegrationSession[]
} }
model DifySetting { model DifySetting {
@ -489,3 +484,91 @@ model DifySetting {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
} }
model GenericBot {
id String @id @default(cuid())
enabled Boolean @default(true)
description String? @db.VarChar(255)
apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false)
debounceTime Int? @db.Int
ignoreJids Json?
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
GenericSetting GenericSetting[]
}
model GenericSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false)
debounceTime Int? @db.Int
ignoreJids Json?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback GenericBot? @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)
description String? @db.VarChar(255)
apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false)
debounceTime Int? @db.Int
ignoreJids Json?
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @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.Int
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Int
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false)
debounceTime Int? @db.Int
ignoreJids Json?
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @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
}

View File

@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the column `difyId` on the `IntegrationSession` table. All the data in the column will be lost.
- You are about to drop the column `openaiBotId` on the `IntegrationSession` table. All the data in the column will be lost.
- You are about to drop the column `typebotId` on the `IntegrationSession` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_difyId_fkey";
-- DropForeignKey
ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_openaiBotId_fkey";
-- DropForeignKey
ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_typebotId_fkey";
-- AlterTable
ALTER TABLE "IntegrationSession" DROP COLUMN "difyId",
DROP COLUMN "openaiBotId",
DROP COLUMN "typebotId",
ADD COLUMN "botId" TEXT;

View File

@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "GenericBot" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"description" VARCHAR(255),
"apiUrl" 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,
"triggerType" "TriggerType",
"triggerOperator" "TriggerOperator",
"triggerValue" TEXT,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "GenericBot_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GenericSetting" (
"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,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"botIdFallback" VARCHAR(100),
"instanceId" TEXT NOT NULL,
CONSTRAINT "GenericSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "GenericSetting_instanceId_key" ON "GenericSetting"("instanceId");
-- AddForeignKey
ALTER TABLE "GenericBot" ADD CONSTRAINT "GenericBot_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_botIdFallback_fkey" FOREIGN KEY ("botIdFallback") REFERENCES "GenericBot"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "Flowise" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"description" VARCHAR(255),
"apiUrl" 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,
"triggerType" "TriggerType",
"triggerOperator" "TriggerOperator",
"triggerValue" TEXT,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "Flowise_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FlowiseSetting" (
"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,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"flowiseIdFallback" VARCHAR(100),
"instanceId" TEXT NOT NULL,
CONSTRAINT "FlowiseSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "FlowiseSetting_instanceId_key" ON "FlowiseSetting"("instanceId");
-- AddForeignKey
ALTER TABLE "Flowise" ADD CONSTRAINT "Flowise_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FlowiseSetting" ADD CONSTRAINT "FlowiseSetting_flowiseIdFallback_fkey" FOREIGN KEY ("flowiseIdFallback") REFERENCES "Flowise"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FlowiseSetting" ADD CONSTRAINT "FlowiseSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -100,6 +100,10 @@ model Instance {
Dify Dify[] Dify Dify[]
DifySetting DifySetting? DifySetting DifySetting?
integrationSessions IntegrationSession[] integrationSessions IntegrationSession[]
GenericBot GenericBot[]
GenericSetting GenericSetting?
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
} }
model Session { model Session {
@ -285,29 +289,28 @@ model Websocket {
} }
model Typebot { model Typebot {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255) description String? @db.VarChar(255)
url String @db.VarChar(500) url String @db.VarChar(500)
typebot String @db.VarChar(100) typebot String @db.VarChar(100)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp updatedAt DateTime? @updatedAt @db.Timestamp
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
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[]
sessions IntegrationSession[]
} }
model TypebotSetting { model TypebotSetting {
@ -354,37 +357,36 @@ model OpenaiCreds {
} }
model OpenaiBot { model OpenaiBot {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255) description String? @db.VarChar(255)
botType OpenaiBotType botType OpenaiBotType
assistantId String? @db.VarChar(255) assistantId String? @db.VarChar(255)
functionUrl String? @db.VarChar(500) functionUrl String? @db.VarChar(500)
model String? @db.VarChar(100) model String? @db.VarChar(100)
systemMessages Json? @db.JsonB systemMessages Json? @db.JsonB
assistantMessages Json? @db.JsonB assistantMessages Json? @db.JsonB
userMessages Json? @db.JsonB userMessages Json? @db.JsonB
maxTokens Int? @db.Integer maxTokens Int? @db.Integer
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade) OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
openaiCredsId String openaiCredsId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
OpenaiSetting OpenaiSetting[] OpenaiSetting OpenaiSetting[]
sessions IntegrationSession[]
} }
model IntegrationSession { model IntegrationSession {
@ -402,14 +404,7 @@ model IntegrationSession {
instanceId String instanceId String
parameters Json? @db.JsonB parameters Json? @db.JsonB
OpenaiBot OpenaiBot? @relation(fields: [openaiBotId], references: [id], onDelete: Cascade) botId String?
openaiBotId String?
DifyBot Dify? @relation(fields: [difyId], references: [id], onDelete: Cascade)
difyId String?
Typebot Typebot? @relation(fields: [typebotId], references: [id], onDelete: Cascade)
typebotId String?
} }
model OpenaiSetting { model OpenaiSetting {
@ -447,30 +442,29 @@ model Template {
} }
model Dify { model Dify {
id String @id @default(cuid()) id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255) description String? @db.VarChar(255)
botType DifyBotType botType DifyBotType
apiUrl String? @db.VarChar(255) apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255) apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
triggerValue String? triggerValue String?
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 instanceId String
DifySetting DifySetting[] DifySetting DifySetting[]
sessions IntegrationSession[]
} }
model DifySetting { model DifySetting {
@ -491,3 +485,91 @@ model DifySetting {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique instanceId String @unique
} }
model GenericBot {
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?
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
GenericSetting GenericSetting[]
}
model GenericSetting {
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?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback GenericBot? @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?
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?
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
}

View File

@ -36,11 +36,7 @@ export class InstanceController {
public async createInstance(instanceData: InstanceDto) { public async createInstance(instanceData: InstanceDto) {
try { try {
if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) { const instance = channelController.init(instanceData, {
throw new BadRequestException('token is required');
}
const instance = channelController.init(instanceData.integration, {
configService: this.configService, configService: this.configService,
eventEmitter: this.eventEmitter, eventEmitter: this.eventEmitter,
prismaRepository: this.prismaRepository, prismaRepository: this.prismaRepository,
@ -52,6 +48,8 @@ export class InstanceController {
const instanceId = v4(); const instanceId = v4();
instanceData.instanceId = instanceId;
let hash: string; let hash: string;
if (!instanceData.token) hash = v4().toUpperCase(); if (!instanceData.token) hash = v4().toUpperCase();
@ -75,16 +73,16 @@ export class InstanceController {
businessId: instanceData.businessId, businessId: instanceData.businessId,
}); });
instance.sendDataWebhook(Events.INSTANCE_CREATE, {
instanceName: instanceData.instanceName,
instanceId: instanceId,
});
this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName); this.waMonitor.delInstanceTime(instance.instanceName);
// set events // set events
eventController.setInstance(instance.instanceName, instanceData); await eventController.setInstance(instance.instanceName, instanceData);
instance.sendDataWebhook(Events.INSTANCE_CREATE, {
instanceName: instanceData.instanceName,
instanceId: instanceId,
});
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) { if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
const testProxy = await this.proxyService.testProxy({ const testProxy = await this.proxyService.testProxy({

View File

@ -0,0 +1,12 @@
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
}
export class IgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

@ -1,10 +1,13 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { ProviderFiles } from '@api/provider/sessions'; import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { CacheService } from '@api/services/cache.service'; import { CacheService } from '@api/services/cache.service';
import { Integration } from '@api/types/wa.types'; import { Integration } from '@api/types/wa.types';
import { ConfigService } from '@config/env.config'; import { ConfigService } from '@config/env.config';
import { BadRequestException } from '@exceptions';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { EvolutionStartupService } from './evolution/evolution.channel.service';
import { BaileysStartupService } from './whatsapp/baileys/whatsapp.baileys.service'; import { BaileysStartupService } from './whatsapp/baileys/whatsapp.baileys.service';
import { BusinessStartupService } from './whatsapp/business/whatsapp.business.service'; import { BusinessStartupService } from './whatsapp/business/whatsapp.business.service';
@ -19,8 +22,12 @@ type ChannelDataType = {
}; };
export class ChannelController { export class ChannelController {
public init(integration: string, data: ChannelDataType) { public init(instanceData: InstanceDto, data: ChannelDataType) {
if (integration === Integration.WHATSAPP_BUSINESS) { if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) {
throw new BadRequestException('token is required');
}
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
return new BusinessStartupService( return new BusinessStartupService(
data.configService, data.configService,
data.eventEmitter, data.eventEmitter,
@ -32,6 +39,18 @@ export class ChannelController {
); );
} }
if (instanceData.integration === Integration.EVOLUTION) {
return new EvolutionStartupService(
data.configService,
data.eventEmitter,
data.prismaRepository,
data.cache,
data.chatwootCache,
data.baileysCache,
data.providerFiles,
);
}
return new BaileysStartupService( return new BaileysStartupService(
data.configService, data.configService,
data.eventEmitter, data.eventEmitter,

View File

@ -0,0 +1,475 @@
import { Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { chatbotController } from '@api/server.module';
import { CacheService } from '@api/services/cache.service';
import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Openai } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import EventEmitter2 from 'eventemitter2';
export class EvolutionStartupService extends ChannelStartupService {
constructor(
public readonly configService: ConfigService,
public readonly eventEmitter: EventEmitter2,
public readonly prismaRepository: PrismaRepository,
public readonly cache: CacheService,
public readonly chatwootCache: CacheService,
public readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) {
super(configService, eventEmitter, prismaRepository, chatwootCache);
this.client = null;
}
public client: any;
public stateConnection: wa.StateConnection = { state: 'open' };
public phoneNumber: string;
public mobile: boolean;
public get connectionStatus() {
return this.stateConnection;
}
public async closeClient() {
this.stateConnection = { state: 'close' };
}
public get qrCode(): wa.QrCode {
return {
pairingCode: this.instance.qrcode?.pairingCode,
code: this.instance.qrcode?.code,
base64: this.instance.qrcode?.base64,
count: this.instance.qrcode?.count,
};
}
public async logoutInstance() {
await this.closeClient();
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
return {
wuid: jid,
profilePictureUrl: null,
};
}
public async getProfileName() {
return null;
}
public async profilePictureUrl() {
return null;
}
public async getProfileStatus() {
return null;
}
public async connectToWhatsapp(data?: any): Promise<any> {
if (!data) return;
try {
this.loadChatwoot();
this.eventHandler(data);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());
}
}
protected async eventHandler(received: any) {
try {
let messageRaw: any;
if (received.message) {
const key = {
id: received.key.id,
remoteJid: received.key.remoteJid,
fromMe: received.key.fromMe,
};
messageRaw = {
key,
pushName: received.pushName,
message: received.message,
messageType: received.messageType,
messageTimestamp: Math.round(new Date().getTime() / 1000),
source: 'unknown',
instanceId: this.instanceId,
};
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.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
await chatbotController.emit({
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
remoteJid: messageRaw.key.remoteJid,
msg: messageRaw,
pushName: messageRaw.pushName,
});
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled) {
const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
Events.MESSAGES_UPSERT,
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw,
);
if (chatwootSentMessage?.id) {
messageRaw.chatwootMessageId = chatwootSentMessage.id;
messageRaw.chatwootInboxId = chatwootSentMessage.id;
messageRaw.chatwootConversationId = chatwootSentMessage.id;
}
}
await this.prismaRepository.message.create({
data: messageRaw,
});
const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
});
const contactRaw: any = {
remoteJid: messageRaw.key.remoteJid,
pushName: received.pushName,
instanceId: this.instanceId,
};
if (contactRaw.remoteJid === 'status@broadcast') {
return;
}
if (contact) {
const contactRaw: any = {
remoteJid: messageRaw.key.remoteJid,
pushName: received.pushName,
instanceId: this.instanceId,
};
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 },
data: contactRaw,
});
return;
}
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
this.prismaRepository.contact.create({
data: contactRaw,
});
}
} catch (error) {
this.logger.error(error);
}
}
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
try {
let quoted: any;
let webhookUrl: any;
if (options?.quoted) {
const m = options?.quoted;
const msg = m?.key;
if (!msg) {
throw 'Message not found';
}
quoted = msg;
}
if (options?.webhookUrl) {
webhookUrl = options.webhookUrl;
}
const messageRaw: any = {
key: { fromMe: true, id: 'ID', remoteJid: this.createJid(number) },
message: {
...message,
quoted,
},
messageType: 'conversation',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
this.logger.log(messageRaw);
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && !isIntegration) {
this.chatwootService.eventWhatsapp(
Events.SEND_MESSAGE,
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw,
);
}
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration)
await chatbotController.emit({
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
remoteJid: messageRaw.key.remoteJid,
msg: messageRaw,
pushName: messageRaw.pushName,
});
await this.prismaRepository.message.create({
data: messageRaw,
});
return messageRaw;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.toString());
}
}
public async textMessage(data: SendTextDto, isIntegration = false) {
const res = await this.sendMessageWithTyping(
data.number,
{
conversation: data.text,
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
isIntegration,
);
return res;
}
public async mediaMessage(data: SendMediaDto, isIntegration = false) {
const message = data;
return await this.sendMessageWithTyping(
data.number,
{ ...message },
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
isIntegration,
);
}
public async audioWhatsapp(data: SendAudioDto, isIntegration = false) {
const message = data;
return await this.sendMessageWithTyping(
data.number,
{ ...message },
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
isIntegration,
);
}
public async buttonMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async locationMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async listMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async templateMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async contactMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async reactionMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async getBase64FromMediaMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async deleteMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async mediaSticker() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async pollMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async statusMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async reloadConnection() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async whatsappNumber() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async markMessageAsRead() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async archiveChat() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async markChatUnread() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fetchProfile() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async sendPresence() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async setPresence() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fetchPrivacySettings() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updatePrivacySettings() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fetchBusinessProfile() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateProfileName() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateProfileStatus() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateProfilePicture() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async removeProfilePicture() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async blockUser() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async createGroup() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateGroupPicture() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateGroupSubject() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateGroupDescription() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async findGroup() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fetchAllGroups() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async inviteCode() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async inviteInfo() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async sendInvite() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async acceptInviteCode() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async revokeInviteCode() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async findParticipants() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateGParticipant() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async updateGSetting() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async toggleEphemeral() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async leaveGroup() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fetchLabels() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async handleLabel() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async receiveMobileCode() {
throw new BadRequestException('Method not available on Evolution Channel');
}
public async fakeCall() {
throw new BadRequestException('Method not available on Evolution Channel');
}
}

View File

@ -60,13 +60,11 @@ import {
configService, configService,
ConfigSessionPhone, ConfigSessionPhone,
Database, Database,
Dify,
Log, Log,
Openai, Openai,
ProviderSession, ProviderSession,
QrCode, QrCode,
S3, S3,
Typebot,
} from '@config/env.config'; } from '@config/env.config';
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions';
import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import ffmpegPath from '@ffmpeg-installer/ffmpeg';
@ -2012,35 +2010,14 @@ export class BaileysStartupService extends ChannelStartupService {
); );
} }
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration) { if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration)
if (this.configService.get<Typebot>('TYPEBOT').ENABLED) { await chatbotController.emit({
if (messageRaw.messageType !== 'reactionMessage') instance: { instanceName: this.instance.name, instanceId: this.instanceId },
await this.typebotService.sendTypebot( remoteJid: messageRaw.key.remoteJid,
{ instanceName: this.instance.name, instanceId: this.instanceId }, msg: messageRaw,
messageRaw.key.remoteJid, pushName: messageRaw.pushName,
messageRaw, isIntegration,
); });
}
if (this.configService.get<Openai>('OPENAI').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.openaiService.sendOpenai(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw.pushName,
messageRaw,
);
}
if (this.configService.get<Dify>('DIFY').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.difyService.sendDify(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw,
);
}
}
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE)
await this.prismaRepository.message.create({ await this.prismaRepository.message.create({

View File

@ -20,7 +20,7 @@ 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, Database, Dify, Openai, S3, Typebot, 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 axios from 'axios'; import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator'; import { arrayUnique, isURL } from 'class-validator';
@ -85,17 +85,10 @@ export class BusinessStartupService extends ChannelStartupService {
public async profilePicture(number: string) { public async profilePicture(number: string) {
const jid = this.createJid(number); const jid = this.createJid(number);
try { return {
return { wuid: jid,
wuid: jid, profilePictureUrl: null,
profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), };
};
} catch (error) {
return {
wuid: jid,
profilePictureUrl: null,
};
}
} }
public async getProfileName() { public async getProfileName() {
@ -923,35 +916,13 @@ export class BusinessStartupService extends ChannelStartupService {
); );
} }
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration) { if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration)
if (this.configService.get<Typebot>('TYPEBOT').ENABLED) { await chatbotController.emit({
if (messageRaw.messageType !== 'reactionMessage') instance: { instanceName: this.instance.name, instanceId: this.instanceId },
await this.typebotService.sendTypebot( remoteJid: messageRaw.key.remoteJid,
{ instanceName: this.instance.name, instanceId: this.instanceId }, msg: messageRaw,
messageRaw.key.remoteJid, pushName: messageRaw.pushName,
messageRaw, });
);
}
if (this.configService.get<Openai>('OPENAI').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.openaiService.sendOpenai(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw.pushName,
messageRaw,
);
}
if (this.configService.get<Dify>('DIFY').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.difyService.sendDify(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw,
);
}
}
await this.prismaRepository.message.create({ await this.prismaRepository.message.create({
data: messageRaw, data: messageRaw,

View File

@ -1,12 +1,54 @@
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 { difyController, openaiController, typebotController, websocketController } from '@api/server.module'; import {
difyController,
flowiseController,
genericController,
openaiController,
typebotController,
websocketController,
} from '@api/server.module';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';
import { findBotByTrigger } from '@utils/findBotByTrigger';
export type EmitData = {
instance: InstanceDto;
remoteJid: string;
msg: any;
pushName?: string;
};
export interface ChatbotControllerInterface {
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } };
createBot(instance: InstanceDto, data: any): Promise<any>;
findBot(instance: InstanceDto): Promise<any>;
fetchBot(instance: InstanceDto, botId: string): Promise<any>;
updateBot(instance: InstanceDto, botId: string, data: any): Promise<any>;
deleteBot(instance: InstanceDto, botId: string): Promise<any>;
settings(instance: InstanceDto, data: any): Promise<any>;
fetchSettings(instance: InstanceDto): Promise<any>;
changeStatus(instance: InstanceDto, botId: string, status: string): Promise<any>;
fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string): Promise<any>;
ignoreJid(instance: InstanceDto, data: any): Promise<any>;
emit(data: EmitData): Promise<void>;
}
export class ChatbotController { export class ChatbotController {
public prismaRepository: PrismaRepository; public prismaRepository: PrismaRepository;
public waMonitor: WAMonitoringService; public waMonitor: WAMonitoringService;
public readonly logger = new Logger(ChatbotController.name);
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
this.prisma = prismaRepository; this.prisma = prismaRepository;
this.monitor = waMonitor; this.monitor = waMonitor;
@ -33,18 +75,24 @@ export class ChatbotController {
remoteJid, remoteJid,
msg, msg,
pushName, pushName,
isIntegration = false,
}: { }: {
instance: InstanceDto; instance: InstanceDto;
remoteJid: string; remoteJid: string;
msg: any; msg: any;
pushName?: string; pushName?: string;
isIntegration?: boolean;
}): Promise<void> { }): Promise<void> {
const emitData = { const emitData = {
instance, instance,
remoteJid, remoteJid,
msg, msg,
pushName, pushName,
isIntegration,
}; };
// generic
await genericController.emit(emitData);
// typebot // typebot
await typebotController.emit(emitData); await typebotController.emit(emitData);
@ -53,6 +101,9 @@ export class ChatbotController {
// dify // dify
await difyController.emit(emitData); await difyController.emit(emitData);
// flowise
await flowiseController.emit(emitData);
} }
public async setInstance(instanceName: string, data: any): Promise<any> { public async setInstance(instanceName: string, data: any): Promise<any> {
@ -63,4 +114,112 @@ export class ChatbotController {
events: data.websocketEvents, events: data.websocketEvents,
}); });
} }
public processDebounce(
userMessageDebounce: any,
content: string,
remoteJid: string,
debounceTime: number,
callback: any,
) {
if (userMessageDebounce[remoteJid]) {
userMessageDebounce[remoteJid].message += ` ${content}`;
this.logger.log('message debounced: ' + userMessageDebounce[remoteJid].message);
clearTimeout(userMessageDebounce[remoteJid].timeoutId);
} else {
userMessageDebounce[remoteJid] = {
message: content,
timeoutId: null,
};
}
userMessageDebounce[remoteJid].timeoutId = setTimeout(() => {
const myQuestion = userMessageDebounce[remoteJid].message;
this.logger.log('Debounce complete. Processing message: ' + myQuestion);
delete userMessageDebounce[remoteJid];
callback(myQuestion);
}, debounceTime * 1000);
}
public checkIgnoreJids(ignoreJids: any, remoteJid: string) {
if (ignoreJids && ignoreJids.length > 0) {
let ignoreGroups = false;
let ignoreContacts = false;
if (ignoreJids.includes('@g.us')) {
ignoreGroups = true;
}
if (ignoreJids.includes('@s.whatsapp.net')) {
ignoreContacts = true;
}
if (ignoreGroups && remoteJid.endsWith('@g.us')) {
this.logger.warn('Ignoring message from group: ' + remoteJid);
return true;
}
if (ignoreContacts && remoteJid.endsWith('@s.whatsapp.net')) {
this.logger.warn('Ignoring message from contact: ' + remoteJid);
return true;
}
if (ignoreJids.includes(remoteJid)) {
this.logger.warn('Ignoring message from jid: ' + remoteJid);
return true;
}
return false;
}
return false;
}
public async getSession(remoteJid: string, instance: InstanceDto) {
let session = await this.prismaRepository.integrationSession.findFirst({
where: {
remoteJid: remoteJid,
instanceId: instance.instanceId,
},
orderBy: { createdAt: 'desc' },
});
if (session) {
if (session.status !== 'closed' && !session.botId) {
this.logger.warn('Session is already opened in another integration');
return;
} else if (!session.botId) {
session = null;
}
}
return session;
}
public async findBotTrigger(
botRepository: any,
settingsRepository: any,
content: string,
instance: InstanceDto,
session?: IntegrationSession,
) {
let findBot = null;
if (!session) {
findBot = await findBotByTrigger(botRepository, settingsRepository, content, instance.instanceId);
if (!findBot) {
return;
}
} else {
findBot = await botRepository.findFirst({
where: {
id: session.botId,
},
});
}
return findBot;
}
} }

View File

@ -4,15 +4,20 @@ 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 { FlowiseRouter } from './flowise/routes/flowise.router';
import { GenericRouter } from './generic/routes/generic.router';
export class ChatbotRouter { export class ChatbotRouter {
public readonly router: Router; public readonly router: Router;
constructor(...guards: any[]) { constructor(...guards: any[]) {
this.router = Router(); this.router = Router();
this.router.use('/generic', new GenericRouter(...guards).router);
this.router.use('/chatwoot', new ChatwootRouter(...guards).router); this.router.use('/chatwoot', new ChatwootRouter(...guards).router);
this.router.use('/typebot', new TypebotRouter(...guards).router); this.router.use('/typebot', new TypebotRouter(...guards).router);
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);
} }
} }

View File

@ -1,4 +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/flowise/validate/flowise.schema';
export * from '@api/integrations/chatbot/generic/validate/generic.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

@ -215,7 +215,13 @@ export class ChatwootService {
inboxId = inbox.id; inboxId = inbox.id;
} }
this.logger.log(`Inox created - inboxId: ${inboxId}`); this.logger.log(`Inbox created - inboxId: ${inboxId}`);
if (!this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT) {
this.logger.log('Chatwoot bot contact is disabled');
return true;
}
this.logger.log('Creating chatwoot bot contact'); this.logger.log('Creating chatwoot bot contact');
const contact = const contact =
@ -826,6 +832,12 @@ export class ChatwootService {
return null; return null;
} }
if (!this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT) {
this.logger.log('Chatwoot bot contact is disabled');
return true;
}
const contact = await this.findContact(instance, '123456'); const contact = await this.findContact(instance, '123456');
if (!contact) { if (!contact) {
@ -940,6 +952,12 @@ export class ChatwootService {
return null; return null;
} }
if (!this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT) {
this.logger.log('Chatwoot bot contact is disabled');
return true;
}
const contact = await this.findContact(instance, '123456'); const contact = await this.findContact(instance, '123456');
if (!contact) { if (!contact) {
@ -1159,7 +1177,9 @@ export class ChatwootService {
return { message: 'bot' }; return { message: 'bot' };
} }
if (chatId === '123456' && body.message_type === 'outgoing') { const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
if (cwBotContact && chatId === '123456' && body.message_type === 'outgoing') {
const command = messageReceived.replace('/', ''); const command = messageReceived.replace('/', '');
if (command.includes('init') || command.includes('iniciar')) { if (command.includes('init') || command.includes('iniciar')) {

View File

@ -1,84 +1,825 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { DifyDto, DifyIgnoreJidDto } 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';
import { PrismaRepository } from '@api/repository/repository.service';
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 { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { getConversationMessage } from '@utils/getConversationMessage';
export class DifyController { import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
constructor(private readonly difyService: DifyService) {}
public async createDify(instance: InstanceDto, data: DifyDto) { export class DifyController extends ChatbotController implements ChatbotControllerInterface {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); constructor(
private readonly difyService: DifyService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
return this.difyService.create(instance, data); this.botRepository = this.prismaRepository.dify;
this.settingsRepository = this.prismaRepository.difySetting;
this.sessionRepository = this.prismaRepository.integrationSession;
} }
public async findDify(instance: InstanceDto) { public readonly logger = new Logger(DifyController.name);
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.find(instance); integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids
) {
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 (!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 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,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating dify');
}
} }
public async fetchDify(instance: InstanceDto, difyId: string) { public async findBot(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetch(instance, difyId); 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 updateDify(instance: InstanceDto, difyId: string, data: DifyDto) { public async fetchBot(instance: InstanceDto, botId: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.update(instance, difyId, data); const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return bot;
} }
public async deleteDify(instance: InstanceDto, difyId: string) { public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.delete(instance, difyId); const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
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,
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,
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,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating dify');
}
} }
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting dify bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) { public async settings(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.setDefaultSettings(instance, data); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
},
});
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,
};
}
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,
},
});
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,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
} }
public async fetchSettings(instance: InstanceDto) { public async fetchSettings(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchDefaultSettings(instance); 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: [],
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,
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) { public async changeStatus(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.changeStatus(instance, data); 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, difyId: string) { public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchSessions(instance, difyId); 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 },
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
} }
public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) { public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.ignoreJid(instance, data); 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({ // Emit
instance, public async emit({ instance, remoteJid, msg }: EmitData) {
remoteJid, if (!this.integrationEnabled) return;
msg,
}: {
instance: InstanceDto;
remoteJid: string;
msg: any;
pushName?: string;
}) {
if (!configService.get<Dify>('DIFY').ENABLED) return;
await this.difyService.sendDify(instance, remoteJid, msg); 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);
const findBot = await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
);
if (!findBot) 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;
if (
!expire ||
!keywordFinish ||
!delayMessage ||
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen ||
!debounceTime
) {
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;
}
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 (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,
debouncedContent,
msg?.pushName,
);
});
} else {
await this.difyService.processDify(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
} }
} }

View File

@ -1,13 +1,5 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
}
export class DifyDto { export class DifyDto {
enabled?: boolean; enabled?: boolean;
description?: string; description?: string;
@ -40,8 +32,3 @@ export class DifySettingDto {
difyIdFallback?: string; difyIdFallback?: string;
ignoreJids?: any; ignoreJids?: any;
} }
export class DifyIgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

@ -1,6 +1,7 @@
import { RouterBroker } from '@api/abstract/abstract.router'; import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '@api/integrations/chatbot/dify/dto/dify.dto'; import { DifyDto, DifySettingDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
import { HttpStatus } from '@api/routes/index.router'; import { HttpStatus } from '@api/routes/index.router';
import { difyController } from '@api/server.module'; import { difyController } from '@api/server.module';
import { import {
@ -21,7 +22,7 @@ export class DifyRouter extends RouterBroker {
request: req, request: req,
schema: difySchema, schema: difySchema,
ClassRef: DifyDto, ClassRef: DifyDto,
execute: (instance, data) => difyController.createDify(instance, data), execute: (instance, data) => difyController.createBot(instance, data),
}); });
res.status(HttpStatus.CREATED).json(response); res.status(HttpStatus.CREATED).json(response);
@ -31,7 +32,7 @@ export class DifyRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => difyController.findDify(instance), execute: (instance) => difyController.findBot(instance),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -41,7 +42,7 @@ export class DifyRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => difyController.fetchDify(instance, req.params.difyId), execute: (instance) => difyController.fetchBot(instance, req.params.difyId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -51,7 +52,7 @@ export class DifyRouter extends RouterBroker {
request: req, request: req,
schema: difySchema, schema: difySchema,
ClassRef: DifyDto, ClassRef: DifyDto,
execute: (instance, data) => difyController.updateDify(instance, req.params.difyId, data), execute: (instance, data) => difyController.updateBot(instance, req.params.difyId, data),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -61,7 +62,7 @@ export class DifyRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => difyController.deleteDify(instance, req.params.difyId), execute: (instance) => difyController.deleteBot(instance, req.params.difyId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -107,10 +108,10 @@ export class DifyRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<DifyIgnoreJidDto>({ const response = await this.dataValidate<IgnoreJidDto>({
request: req, request: req,
schema: difyIgnoreJidSchema, schema: difyIgnoreJidSchema,
ClassRef: DifyIgnoreJidDto, ClassRef: IgnoreJidDto,
execute: (instance, data) => difyController.ignoreJid(instance, data), execute: (instance, data) => difyController.ignoreJid(instance, data),
}); });

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,797 @@
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 { getConversationMessage } from '@utils/getConversationMessage';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { FlowiseDto } from '../dto/flowise.dto';
import { FlowiseService } from '../services/flowise.service';
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
constructor(
private readonly flowiseService: FlowiseService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.botRepository = this.prismaRepository.flowise;
this.settingsRepository = this.prismaRepository.flowiseSetting;
this.sessionRepository = this.prismaRepository.integrationSession;
}
public readonly logger = new Logger(FlowiseController.name);
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: FlowiseDto) {
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 || '';
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 (!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 Flowise 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('Flowise already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
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,
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,
},
});
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,
},
});
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,
};
}
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,
},
});
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,
};
} 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: [],
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,
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 },
},
});
} 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);
const findBot = await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
);
if (!findBot) 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;
if (
!expire ||
!keywordFinish ||
!delayMessage ||
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen ||
!debounceTime
) {
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;
}
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 (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,
debouncedContent,
msg?.pushName,
);
});
} else {
await this.flowiseService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

@ -0,0 +1,33 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
export class FlowiseDto {
enabled?: boolean;
description?: string;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
}
export class FlowiseSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
flowiseIdFallback?: string;
ignoreJids?: any;
}

View File

@ -0,0 +1,124 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { flowiseController } from '@api/server.module';
import { instanceSchema } from '@validate/instance.schema';
import { RequestHandler, Router } from 'express';
import { FlowiseDto, FlowiseSettingDto } from '../dto/flowise.dto';
import {
flowiseIgnoreJidSchema,
flowiseSchema,
flowiseSettingSchema,
flowiseStatusSchema,
} from '../validate/flowise.schema';
export class FlowiseRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<FlowiseDto>({
request: req,
schema: flowiseSchema,
ClassRef: FlowiseDto,
execute: (instance, data) => flowiseController.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) => flowiseController.findBot(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:flowiseId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => flowiseController.fetchBot(instance, req.params.flowiseId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:flowiseId'), ...guards, async (req, res) => {
const response = await this.dataValidate<FlowiseDto>({
request: req,
schema: flowiseSchema,
ClassRef: FlowiseDto,
execute: (instance, data) => flowiseController.updateBot(instance, req.params.flowiseId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:flowiseId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => flowiseController.deleteBot(instance, req.params.flowiseId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<FlowiseSettingDto>({
request: req,
schema: flowiseSettingSchema,
ClassRef: FlowiseSettingDto,
execute: (instance, data) => flowiseController.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) => flowiseController.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: flowiseStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => flowiseController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:flowiseId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => flowiseController.fetchSessions(instance, req.params.flowiseId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<IgnoreJidDto>({
request: req,
schema: flowiseIgnoreJidSchema,
ClassRef: IgnoreJidDto,
execute: (instance, data) => flowiseController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -0,0 +1,309 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
export class FlowiseService {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
private readonly logger = new Logger('FlowiseService');
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
}
private async sendMessageToBot(
instance: any,
session: IntegrationSession,
bot: Flowise,
remoteJid: string,
pushName: string,
content: string,
) {
const payload: any = {
question: content,
overrideConfig: {
sessionId: remoteJid,
vars: {
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
},
},
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.uploads = [
{
data: contentSplit[1].split('?')[0],
type: 'url',
name: 'Flowise.png',
mime: 'image/png',
},
];
payload.question = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
let headers: any = {
'Content-Type': 'application/json',
};
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
};
}
const endpoint = bot.apiUrl;
if (!endpoint) return null;
const response = await axios.post(endpoint, payload, {
headers,
});
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = response?.data?.text;
return message;
}
private async sendMessageWhatsApp(
instance: any,
remoteJid: string,
session: IntegrationSession,
settings: FlowiseSetting,
message: string,
) {
const regex = /!?\[(.*?)\]\((.*?)\)/g;
const result = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(message)) !== null) {
if (match.index > lastIndex) {
result.push({ text: message.slice(lastIndex, match.index).trim() });
}
result.push({ caption: match[1], url: match[2] });
lastIndex = regex.lastIndex;
}
if (lastIndex < message.length) {
result.push({ text: message.slice(lastIndex).trim() });
}
for (const item of result) {
if (item.text) {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: item.text,
},
false,
);
}
if (item.url) {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'image',
media: item.url,
caption: item.caption,
},
false,
);
}
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
sendTelemetry('/message/sendText');
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,
botId: bot.id,
});
if (data.session) {
session = data.session;
}
const message = await this.sendMessageToBot(instance, session, 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, session, bot, remoteJid, pushName, content);
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
}

View File

@ -0,0 +1,107 @@
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 flowiseSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
apiUrl: { 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' } },
},
required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
};
export const flowiseStatusSchema: 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 flowiseSettingSchema: 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' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
),
};
export const flowiseIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -0,0 +1,797 @@
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 { getConversationMessage } from '@utils/getConversationMessage';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { GenericBotDto } from '../dto/generic.dto';
import { GenericService } from '../services/generic.service';
export class GenericController extends ChatbotController implements ChatbotControllerInterface {
constructor(
private readonly genericService: GenericService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.botRepository = this.prismaRepository.genericBot;
this.settingsRepository = this.prismaRepository.genericSetting;
this.sessionRepository = this.prismaRepository.integrationSession;
}
public readonly logger = new Logger(GenericController.name);
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: GenericBotDto) {
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 || '';
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 (!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 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,
},
});
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: GenericBotDto) {
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,
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,
},
});
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,
botIdFallback: data.botIdFallback,
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,
botIdFallback: updateSettings.botIdFallback,
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,
botIdFallback: data.botIdFallback,
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,
botIdFallback: newSetttings.botIdFallback,
ignoreJids: newSetttings.ignoreJids,
};
} 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: [],
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,
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 },
},
});
} 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);
const findBot = await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
);
if (!findBot) 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;
if (
!expire ||
!keywordFinish ||
!delayMessage ||
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen ||
!debounceTime
) {
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;
}
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 (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.genericService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
debouncedContent,
msg?.pushName,
);
});
} else {
await this.genericService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

@ -0,0 +1,33 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
export class GenericBotDto {
enabled?: boolean;
description?: string;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
}
export class GenericBotSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
botIdFallback?: string;
ignoreJids?: any;
}

View File

@ -0,0 +1,124 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { genericController } from '@api/server.module';
import { instanceSchema } from '@validate/instance.schema';
import { RequestHandler, Router } from 'express';
import { GenericBotDto, GenericBotSettingDto } from '../dto/generic.dto';
import {
genericIgnoreJidSchema,
genericSchema,
genericSettingSchema,
genericStatusSchema,
} from '../validate/generic.schema';
export class GenericRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<GenericBotDto>({
request: req,
schema: genericSchema,
ClassRef: GenericBotDto,
execute: (instance, data) => genericController.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) => genericController.findBot(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:genericId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => genericController.fetchBot(instance, req.params.genericId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:genericId'), ...guards, async (req, res) => {
const response = await this.dataValidate<GenericBotDto>({
request: req,
schema: genericSchema,
ClassRef: GenericBotDto,
execute: (instance, data) => genericController.updateBot(instance, req.params.genericId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:genericId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => genericController.deleteBot(instance, req.params.genericId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<GenericBotSettingDto>({
request: req,
schema: genericSettingSchema,
ClassRef: GenericBotSettingDto,
execute: (instance, data) => genericController.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) => genericController.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: genericStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => genericController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:genericId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => genericController.fetchSessions(instance, req.params.genericId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<IgnoreJidDto>({
request: req,
schema: genericIgnoreJidSchema,
ClassRef: IgnoreJidDto,
execute: (instance, data) => genericController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -0,0 +1,302 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { GenericBot, GenericSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
export class GenericService {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
private readonly logger = new Logger('GenericService');
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
}
private async sendMessageToBot(
instance: any,
session: IntegrationSession,
bot: GenericBot,
remoteJid: string,
pushName: string,
content: string,
) {
const payload: any = {
inputs: {
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
},
query: content,
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
user: remoteJid,
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
url: contentSplit[1].split('?')[0],
},
];
payload.query = contentSplit[2] || content;
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
let headers: any = {
'Content-Type': 'application/json',
};
if (bot.apiKey) {
headers = {
...headers,
Authorization: `Bearer ${bot.apiKey}`,
};
}
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?.answer;
return message;
}
private async sendMessageWhatsApp(
instance: any,
remoteJid: string,
session: IntegrationSession,
settings: GenericSetting,
message: string,
) {
const regex = /!?\[(.*?)\]\((.*?)\)/g;
const result = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(message)) !== null) {
if (match.index > lastIndex) {
result.push({ text: message.slice(lastIndex, match.index).trim() });
}
result.push({ caption: match[1], url: match[2] });
lastIndex = regex.lastIndex;
}
if (lastIndex < message.length) {
result.push({ text: message.slice(lastIndex).trim() });
}
for (const item of result) {
if (item.text) {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: item.text,
},
false,
);
}
if (item.url) {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'image',
media: item.url,
caption: item.caption,
},
false,
);
}
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
sendTelemetry('/message/sendText');
return;
}
private async initNewSession(
instance: any,
remoteJid: string,
bot: GenericBot,
settings: GenericSetting,
session: IntegrationSession,
content: string,
pushName?: string,
) {
const data = await this.createNewSession(instance, {
remoteJid,
botId: bot.id,
});
if (data.session) {
session = data.session;
}
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
public async processBot(
instance: any,
remoteJid: string,
bot: GenericBot,
session: IntegrationSession,
settings: GenericSetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
return;
}
if (session && settings.expire && settings.expire > 0) {
const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > settings.expire) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
}
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
return;
}
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
if (!content) {
if (settings.unknownMessage) {
this.waMonitor.waInstances[instance.instanceName].textMessage(
{
number: remoteJid.split('@')[0],
delay: settings.delayMessage || 1000,
text: settings.unknownMessage,
},
false,
);
sendTelemetry('/message/sendText');
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: bot.id,
remoteJid: remoteJid,
},
});
}
return;
}
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
return;
}
}

View File

@ -0,0 +1,107 @@
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 genericSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
apiUrl: { 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' } },
},
required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
};
export const genericStatusSchema: 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 genericSettingSchema: 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' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
),
};
export const genericIgnoreJidSchema: 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,5 @@
import { TriggerOperator, TriggerType } from '@prisma/client'; import { TriggerOperator, TriggerType } from '@prisma/client';
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
}
export class OpenaiCredsDto { export class OpenaiCredsDto {
name: string; name: string;
apiKey: string; apiKey: string;
@ -53,8 +45,3 @@ export class OpenaiSettingDto {
ignoreJids?: any; ignoreJids?: any;
speechToText?: boolean; speechToText?: boolean;
} }
export class OpenaiIgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

@ -1,11 +1,7 @@
import { RouterBroker } from '@api/abstract/abstract.router'; import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { import { OpenaiCredsDto, OpenaiDto, OpenaiSettingDto } from '@api/integrations/chatbot/openai/dto/openai.dto';
OpenaiCredsDto,
OpenaiDto,
OpenaiIgnoreJidDto,
OpenaiSettingDto,
} from '@api/integrations/chatbot/openai/dto/openai.dto';
import { HttpStatus } from '@api/routes/index.router'; import { HttpStatus } from '@api/routes/index.router';
import { openaiController } from '@api/server.module'; import { openaiController } from '@api/server.module';
import { import {
@ -57,7 +53,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: openaiSchema, schema: openaiSchema,
ClassRef: OpenaiDto, ClassRef: OpenaiDto,
execute: (instance, data) => openaiController.createOpenai(instance, data), execute: (instance, data) => openaiController.createBot(instance, data),
}); });
res.status(HttpStatus.CREATED).json(response); res.status(HttpStatus.CREATED).json(response);
@ -67,7 +63,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => openaiController.findOpenai(instance), execute: (instance) => openaiController.findBot(instance),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -77,7 +73,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => openaiController.fetchOpenai(instance, req.params.openaiBotId), execute: (instance) => openaiController.fetchBot(instance, req.params.openaiBotId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -87,7 +83,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: openaiSchema, schema: openaiSchema,
ClassRef: OpenaiDto, ClassRef: OpenaiDto,
execute: (instance, data) => openaiController.updateOpenai(instance, req.params.openaiBotId, data), execute: (instance, data) => openaiController.updateBot(instance, req.params.openaiBotId, data),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -97,7 +93,7 @@ export class OpenaiRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => openaiController.deleteOpenai(instance, req.params.openaiBotId), execute: (instance) => openaiController.deleteBot(instance, req.params.openaiBotId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -143,10 +139,10 @@ export class OpenaiRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<OpenaiIgnoreJidDto>({ const response = await this.dataValidate<IgnoreJidDto>({
request: req, request: req,
schema: openaiIgnoreJidSchema, schema: openaiIgnoreJidSchema,
ClassRef: OpenaiIgnoreJidDto, ClassRef: IgnoreJidDto,
execute: (instance, data) => openaiController.ignoreJid(instance, data), execute: (instance, data) => openaiController.ignoreJid(instance, data),
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,5 @@
import { TriggerOperator, TriggerType } from '@prisma/client'; import { TriggerOperator, TriggerType } from '@prisma/client';
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
prefilledVariables?: PrefilledVariables;
}
export class PrefilledVariables { export class PrefilledVariables {
remoteJid?: string; remoteJid?: string;
pushName?: string; pushName?: string;
@ -47,8 +38,3 @@ export class TypebotSettingDto {
typebotIdFallback?: string; typebotIdFallback?: string;
ignoreJids?: any; ignoreJids?: any;
} }
export class TypebotIgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

@ -1,8 +1,9 @@
import { RouterBroker } from '@api/abstract/abstract.router'; import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { TypebotDto, TypebotIgnoreJidDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { TypebotDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
import { typebotController } from '@api/server.module';
import { HttpStatus } from '@api/routes/index.router'; import { HttpStatus } from '@api/routes/index.router';
import { typebotController } from '@api/server.module';
import { import {
instanceSchema, instanceSchema,
typebotIgnoreJidSchema, typebotIgnoreJidSchema,
@ -22,7 +23,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: typebotSchema, schema: typebotSchema,
ClassRef: TypebotDto, ClassRef: TypebotDto,
execute: (instance, data) => typebotController.createTypebot(instance, data), execute: (instance, data) => typebotController.createBot(instance, data),
}); });
res.status(HttpStatus.CREATED).json(response); res.status(HttpStatus.CREATED).json(response);
@ -32,7 +33,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => typebotController.findTypebot(instance), execute: (instance) => typebotController.findBot(instance),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -42,7 +43,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => typebotController.fetchTypebot(instance, req.params.typebotId), execute: (instance) => typebotController.fetchBot(instance, req.params.typebotId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -52,7 +53,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: typebotSchema, schema: typebotSchema,
ClassRef: TypebotDto, ClassRef: TypebotDto,
execute: (instance, data) => typebotController.updateTypebot(instance, req.params.typebotId, data), execute: (instance, data) => typebotController.updateBot(instance, req.params.typebotId, data),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -62,7 +63,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: instanceSchema, schema: instanceSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance) => typebotController.deleteTypebot(instance, req.params.typebotId), execute: (instance) => typebotController.deleteBot(instance, req.params.typebotId),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -92,7 +93,7 @@ export class TypebotRouter extends RouterBroker {
request: req, request: req,
schema: typebotStartSchema, schema: typebotStartSchema,
ClassRef: InstanceDto, ClassRef: InstanceDto,
execute: (instance, data) => typebotController.startTypebot(instance, data), execute: (instance, data) => typebotController.startBot(instance, data),
}); });
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
@ -118,10 +119,10 @@ export class TypebotRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<TypebotIgnoreJidDto>({ const response = await this.dataValidate<IgnoreJidDto>({
request: req, request: req,
schema: typebotIgnoreJidSchema, schema: typebotIgnoreJidSchema,
ClassRef: TypebotIgnoreJidDto, ClassRef: IgnoreJidDto,
execute: (instance, data) => typebotController.ignoreJid(instance, data), execute: (instance, data) => typebotController.ignoreJid(instance, data),
}); });

View File

@ -3,6 +3,25 @@ import { rabbitmqController, sqsController, webhookController, websocketControll
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Server } from 'http'; import { Server } from 'http';
export type EmitData = {
instanceName: string;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
local?: boolean;
};
export interface EventControllerInterface {
integrationEnabled: boolean;
set(instanceName: string, data: any): Promise<any>;
get(instanceName: string): Promise<any>;
emit({ instanceName, origin, event, data, serverUrl, dateTime, sender, apiKey, local }: EmitData): Promise<void>;
}
export class EventController { export class EventController {
public prismaRepository: PrismaRepository; public prismaRepository: PrismaRepository;
public waMonitor: WAMonitoringService; public waMonitor: WAMonitoringService;

View File

@ -7,19 +7,19 @@ import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions'; import { NotFoundException } from '@exceptions';
import * as amqp from 'amqplib/callback_api'; import * as amqp from 'amqplib/callback_api';
import { EventController } from '../../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../../event.controller';
export class RabbitmqController extends EventController { export class RabbitmqController extends EventController implements EventControllerInterface {
public amqpChannel: amqp.Channel | null = null; public amqpChannel: amqp.Channel | null = null;
private readonly logger = new Logger(RabbitmqController.name); private readonly logger = new Logger(RabbitmqController.name);
integrationEnabled = configService.get<Rabbitmq>('RABBITMQ')?.ENABLED;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor); super(prismaRepository, waMonitor);
} }
public async init(): Promise<void> { public async init(): Promise<void> {
if (!configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) { if (!this.integrationEnabled) return;
return;
}
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI; const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
@ -62,6 +62,8 @@ export class RabbitmqController extends EventController {
} }
public async set(instanceName: string, data: RabbitmqDto): Promise<wa.LocalRabbitmq> { public async set(instanceName: string, data: RabbitmqDto): Promise<wa.LocalRabbitmq> {
if (!this.integrationEnabled) return;
if (!data.enabled) { if (!data.enabled) {
data.events = []; data.events = [];
} else { } else {
@ -91,6 +93,8 @@ export class RabbitmqController extends EventController {
} }
public async get(instanceName: string): Promise<wa.LocalWebsocket> { public async get(instanceName: string): Promise<wa.LocalWebsocket> {
if (!this.integrationEnabled) return;
if (undefined === this.monitor.waInstances[instanceName]) { if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found'); throw new NotFoundException('Instance not found');
} }
@ -117,19 +121,8 @@ export class RabbitmqController extends EventController {
dateTime, dateTime,
sender, sender,
apiKey, apiKey,
}: { }: EmitData): Promise<void> {
instanceName: string; if (!this.integrationEnabled) return;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) {
return;
}
const instanceRabbitmq = await this.get(instanceName); const instanceRabbitmq = await this.get(instanceName);
const rabbitmqLocal = instanceRabbitmq?.events; const rabbitmqLocal = instanceRabbitmq?.events;

View File

@ -7,20 +7,19 @@ import { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions'; import { NotFoundException } from '@exceptions';
import { EventController } from '../../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../../event.controller';
export class SqsController extends EventController { export class SqsController extends EventController implements EventControllerInterface {
private sqs: SQS; private sqs: SQS;
private readonly logger = new Logger(SqsController.name); private readonly logger = new Logger(SqsController.name);
integrationEnabled = configService.get<Sqs>('SQS')?.ENABLED;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor); super(prismaRepository, waMonitor);
} }
public init(): void { public init(): void {
if (!configService.get<Sqs>('SQS')?.ENABLED) { if (!this.integrationEnabled) return;
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
@ -48,6 +47,8 @@ export class SqsController extends EventController {
} }
public async set(instanceName: string, data: SqsDto): Promise<wa.LocalSqs> { public async set(instanceName: string, data: SqsDto): Promise<wa.LocalSqs> {
if (!this.integrationEnabled) return;
if (!data.enabled) { if (!data.enabled) {
data.events = []; data.events = [];
} else { } else {
@ -77,6 +78,8 @@ export class SqsController extends EventController {
} }
public async get(instanceName: string): Promise<wa.LocalSqs> { public async get(instanceName: string): Promise<wa.LocalSqs> {
if (!this.integrationEnabled) return;
if (undefined === this.monitor.waInstances[instanceName]) { if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found'); throw new NotFoundException('Instance not found');
} }
@ -103,19 +106,8 @@ export class SqsController extends EventController {
dateTime, dateTime,
sender, sender,
apiKey, apiKey,
}: { }: EmitData): Promise<void> {
instanceName: string; if (!this.integrationEnabled) return;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Sqs>('SQS')?.ENABLED) {
return;
}
const instanceSqs = await this.get(instanceName); const instanceSqs = await this.get(instanceName);
const sqsLocal = instanceSqs?.events; const sqsLocal = instanceSqs?.events;

View File

@ -1,21 +1,22 @@
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 { wa } from '@api/types/wa.types'; import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook, Websocket } from '@config/env.config'; import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException, NotFoundException } from '@exceptions'; import { BadRequestException, NotFoundException } from '@exceptions';
import axios from 'axios'; import axios from 'axios';
import { isURL } from 'class-validator'; import { isURL } from 'class-validator';
import { EventController } from '../../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../../event.controller';
import { WebhookDto } from '../dto/webhook.dto'; import { WebhookDto } from '../dto/webhook.dto';
export class WebhookController extends EventController { export class WebhookController extends EventController implements EventControllerInterface {
private readonly logger = new Logger(WebhookController.name); private readonly logger = new Logger(WebhookController.name);
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor); super(prismaRepository, waMonitor);
} }
integrationEnabled: boolean;
public async set(instanceName: string, data: WebhookDto): Promise<wa.LocalWebHook> { public async set(instanceName: string, data: WebhookDto): Promise<wa.LocalWebHook> {
if (!isURL(data.url, { require_tld: false })) { if (!isURL(data.url, { require_tld: false })) {
@ -30,27 +31,24 @@ export class WebhookController extends EventController {
} }
} }
try { await this.get(instanceName);
await this.get(instanceName);
return this.prisma.webhook.update({ return this.prisma.webhook.upsert({
where: { where: {
instanceId: this.monitor.waInstances[instanceName].instanceId, instanceId: this.monitor.waInstances[instanceName].instanceId,
}, },
data, update: {
}); ...data,
} catch (err) { },
return this.prisma.webhook.create({ create: {
data: { enabled: data.enabled,
enabled: data.enabled, events: data.events,
events: data.events, instanceId: this.monitor.waInstances[instanceName].instanceId,
instanceId: this.monitor.waInstances[instanceName].instanceId, url: data.url,
url: data.url, webhookBase64: data.webhookBase64,
webhookBase64: data.webhookBase64, webhookByEvents: data.webhookByEvents,
webhookByEvents: data.webhookByEvents, },
}, });
});
}
} }
public async get(instanceName: string): Promise<wa.LocalWebHook> { public async get(instanceName: string): Promise<wa.LocalWebHook> {
@ -81,23 +79,13 @@ export class WebhookController extends EventController {
sender, sender,
apiKey, apiKey,
local, local,
}: { }: EmitData): Promise<void> {
instanceName: string; const instanceWebhook = await this.get(instanceName);
origin: string; if (!instanceWebhook || !instanceWebhook.enabled) {
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
local?: boolean;
}): Promise<void> {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
return; return;
} }
const instanceWebhook = await this.get(instanceName); const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookGlobal = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instanceWebhook?.events; const webhookLocal = instanceWebhook?.events;
const we = event.replace(/[.-]/gm, '_').toUpperCase(); const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase();
@ -113,6 +101,7 @@ export class WebhookController extends EventController {
server_url: serverUrl, server_url: serverUrl,
apikey: apiKey, apikey: apiKey,
}; };
if (local) { if (local) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string; let baseURL: string;
@ -156,16 +145,12 @@ export class WebhookController extends EventController {
} }
} }
if (webhookGlobal.GLOBAL?.ENABLED) { if (webhookConfig.GLOBAL?.ENABLED) {
if (webhookGlobal.EVENTS[we]) { if (webhookConfig.EVENTS[we]) {
const globalWebhook = configService.get<Webhook>('WEBHOOK').GLOBAL; let globalURL = webhookConfig.GLOBAL.URL;
let globalURL; if (webhookConfig.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalURL}/${transformedWe}`;
if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
} }
if (enabledLog) { if (enabledLog) {
@ -179,7 +164,7 @@ export class WebhookController extends EventController {
} }
try { try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL }); const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', webhookData); await httpService.post('', webhookData);
@ -202,7 +187,7 @@ export class WebhookController extends EventController {
} }
} }
public async receiveWebhook(data: any) { public async receiveWebhookMeta(data: any) {
if (data.object === 'whatsapp_business_account') { if (data.object === 'whatsapp_business_account') {
if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') { if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') {
const template = await this.prismaRepository.template.findFirst({ const template = await this.prismaRepository.template.findFirst({
@ -228,7 +213,7 @@ export class WebhookController extends EventController {
const numberId = entry.changes[0].value.metadata.phone_number_id; const numberId = entry.changes[0].value.metadata.phone_number_id;
if (!numberId) { if (!numberId) {
this.logger.error('WebhookService -> receiveWebhook -> numberId not found'); this.logger.error('WebhookService -> receiveWebhookMeta -> numberId not found');
return; return;
} }
@ -237,7 +222,7 @@ export class WebhookController extends EventController {
}); });
if (!instance) { if (!instance) {
this.logger.error('WebhookService -> receiveWebhook -> instance not found'); this.logger.error('WebhookService -> receiveWebhookMeta -> instance not found');
return; return;
} }
@ -249,4 +234,28 @@ export class WebhookController extends EventController {
return; return;
} }
public async receiveWebhookEvolution(data: any) {
const numberId = data.numberId;
if (!numberId) {
this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found');
return;
}
const instance = await this.prismaRepository.instance.findFirst({
where: { number: numberId },
});
if (!instance) {
this.logger.error('WebhookService -> receiveWebhook -> instance not found');
return;
}
await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
return {
status: 'success',
};
}
} }

View File

@ -1,7 +1,7 @@
import { RouterBroker } from '@api/abstract/abstract.router'; import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { webhookController } from '@api/server.module';
import { HttpStatus } from '@api/routes/index.router'; import { HttpStatus } from '@api/routes/index.router';
import { webhookController } from '@api/server.module';
import { ConfigService, WaBusiness } from '@config/env.config'; import { ConfigService, WaBusiness } from '@config/env.config';
import { instanceSchema, webhookSchema } from '@validate/validate.schema'; import { instanceSchema, webhookSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express'; import { RequestHandler, Router } from 'express';
@ -32,14 +32,20 @@ export class WebhookRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response); res.status(HttpStatus.OK).json(response);
}) })
.get('meta', async (req, res) => { .get(this.routerPath('meta', false), async (req, res) => {
if (req.query['hub.verify_token'] === configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK) if (req.query['hub.verify_token'] === configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK)
res.send(req.query['hub.challenge']); res.send(req.query['hub.challenge']);
else res.send('Error, wrong validation token'); else res.send('Error, wrong validation token');
}) })
.post('meta', async (req, res) => { .post(this.routerPath('meta', false), async (req, res) => {
const { body } = req; const { body } = req;
const response = await webhookController.receiveWebhook(body); const response = await webhookController.receiveWebhookMeta(body);
return res.status(200).json(response);
})
.post(this.routerPath('evolution', false), async (req, res) => {
const { body } = req;
const response = await webhookController.receiveWebhookEvolution(body);
return res.status(200).json(response); return res.status(200).json(response);
}); });

View File

@ -8,12 +8,13 @@ import { NotFoundException } from '@exceptions';
import { Server } from 'http'; import { Server } from 'http';
import { Server as SocketIO } from 'socket.io'; import { Server as SocketIO } from 'socket.io';
import { EventController } from '../../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../../event.controller';
export class WebsocketController extends EventController { export class WebsocketController extends EventController implements EventControllerInterface {
private io: SocketIO; private io: SocketIO;
private corsConfig: Array<any>; private corsConfig: Array<any>;
private readonly logger = new Logger(WebsocketController.name); private readonly logger = new Logger(WebsocketController.name);
integrationEnabled = configService.get<Websocket>('WEBSOCKET')?.ENABLED;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor); super(prismaRepository, waMonitor);
@ -21,9 +22,7 @@ export class WebsocketController extends EventController {
} }
public init(httpServer: Server): void { public init(httpServer: Server): void {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) { if (!this.integrationEnabled) return;
return;
}
this.socket = new SocketIO(httpServer, { this.socket = new SocketIO(httpServer, {
cors: { cors: {
@ -59,6 +58,8 @@ export class WebsocketController extends EventController {
} }
public async set(instanceName: string, data: WebsocketDto): Promise<wa.LocalWebsocket> { public async set(instanceName: string, data: WebsocketDto): Promise<wa.LocalWebsocket> {
if (!this.integrationEnabled) return;
if (!data.enabled) { if (!data.enabled) {
data.events = []; data.events = [];
} else { } else {
@ -88,6 +89,8 @@ export class WebsocketController extends EventController {
} }
public async get(instanceName: string): Promise<wa.LocalWebsocket> { public async get(instanceName: string): Promise<wa.LocalWebsocket> {
if (!this.integrationEnabled) return;
if (undefined === this.monitor.waInstances[instanceName]) { if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found'); throw new NotFoundException('Instance not found');
} }
@ -114,19 +117,8 @@ export class WebsocketController extends EventController {
dateTime, dateTime,
sender, sender,
apiKey, apiKey,
}: { }: EmitData): Promise<void> {
instanceName: string; if (!this.integrationEnabled) return;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
return;
}
const configEv = event.replace(/[.-]/gm, '_').toUpperCase(); const configEv = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET'); const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET');

View File

@ -17,6 +17,10 @@ import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service'; import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller'; import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
import { DifyService } from './integrations/chatbot/dify/services/dify.service'; import { DifyService } from './integrations/chatbot/dify/services/dify.service';
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
import { GenericController } from './integrations/chatbot/generic/controllers/generic.controller';
import { GenericService } from './integrations/chatbot/generic/services/generic.service';
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller'; import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service'; import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller'; import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
@ -108,12 +112,18 @@ export const webhookController = new WebhookController(prismaRepository, waMonit
// chatbots // chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository); const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
export const typebotController = new TypebotController(typebotService); export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
const openaiService = new OpenaiService(waMonitor, configService, prismaRepository); const openaiService = new OpenaiService(waMonitor, configService, prismaRepository);
export const openaiController = new OpenaiController(openaiService); export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
const difyService = new DifyService(waMonitor, configService, prismaRepository); const difyService = new DifyService(waMonitor, configService, prismaRepository);
export const difyController = new DifyController(difyService); export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
const genericService = new GenericService(waMonitor, configService, prismaRepository);
export const genericController = new GenericController(genericService, prismaRepository, waMonitor);
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository);
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
logger.info('Module - ON'); logger.info('Module - ON');

View File

@ -58,11 +58,6 @@ export class ChannelStartupService {
this.instance.token = instance.token; this.instance.token = instance.token;
this.instance.businessId = instance.businessId; this.instance.businessId = instance.businessId;
this.sendDataWebhook(Events.STATUS_INSTANCE, {
instance: this.instance.name,
status: 'created',
});
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled) { if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled) {
this.chatwootService.eventWhatsapp( this.chatwootService.eventWhatsapp(
Events.STATUS_INSTANCE, Events.STATUS_INSTANCE,

View File

@ -1,6 +1,4 @@
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { BaileysStartupService } from '@api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service';
import { BusinessStartupService } from '@api/integrations/channel/whatsapp/business/whatsapp.business.service';
import { ProviderFiles } from '@api/provider/sessions'; import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { channelController } from '@api/server.module'; import { channelController } from '@api/server.module';
@ -37,7 +35,7 @@ export class WAMonitoringService {
private readonly redis: Partial<CacheConf> = {}; private readonly redis: Partial<CacheConf> = {};
private readonly logger = new Logger('WAMonitoringService'); private readonly logger = new Logger('WAMonitoringService');
public readonly waInstances: Record<string, BaileysStartupService | BusinessStartupService> = {}; public readonly waInstances: Record<string, any> = {};
private readonly providerSession = Object.freeze(this.configService.get<ProviderSession>('PROVIDER')); private readonly providerSession = Object.freeze(this.configService.get<ProviderSession>('PROVIDER'));
@ -196,7 +194,7 @@ export class WAMonitoringService {
data: { data: {
id: data.instanceId, id: data.instanceId,
name: data.instanceName, name: data.instanceName,
connectionStatus: data.integration && data.integration === Integration.WHATSAPP_BUSINESS ? 'open' : 'close', connectionStatus: data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : 'open',
number: data.number, number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS, integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: data.hash, token: data.hash,
@ -210,7 +208,7 @@ export class WAMonitoringService {
} }
private async setInstance(instanceData: InstanceDto) { private async setInstance(instanceData: InstanceDto) {
const instance = channelController.init(instanceData.integration, { const instance = channelController.init(instanceData, {
configService: this.configService, configService: this.configService,
eventEmitter: this.eventEmitter, eventEmitter: this.eventEmitter,
prismaRepository: this.prismaRepository, prismaRepository: this.prismaRepository,

View File

@ -146,4 +146,5 @@ export const MessageSubtype = [
export const Integration = { export const Integration = {
WHATSAPP_BUSINESS: 'WHATSAPP-BUSINESS', WHATSAPP_BUSINESS: 'WHATSAPP-BUSINESS',
WHATSAPP_BAILEYS: 'WHATSAPP-BAILEYS', WHATSAPP_BAILEYS: 'WHATSAPP-BAILEYS',
EVOLUTION: 'EVOLUTION',
}; };

View File

@ -181,6 +181,7 @@ export type Chatwoot = {
ENABLED: boolean; ENABLED: boolean;
MESSAGE_DELETE: boolean; MESSAGE_DELETE: boolean;
MESSAGE_READ: boolean; MESSAGE_READ: boolean;
BOT_CONTACT: boolean;
IMPORT: { IMPORT: {
DATABASE: { DATABASE: {
CONNECTION: { CONNECTION: {
@ -426,6 +427,7 @@ export class ConfigService {
ENABLED: process.env?.CHATWOOT_ENABLED === 'true', ENABLED: process.env?.CHATWOOT_ENABLED === 'true',
MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'true', MESSAGE_DELETE: process.env.CHATWOOT_MESSAGE_DELETE === 'true',
MESSAGE_READ: process.env.CHATWOOT_MESSAGE_READ === 'true', MESSAGE_READ: process.env.CHATWOOT_MESSAGE_READ === 'true',
BOT_CONTACT: process.env.CHATWOOT_BOT_CONTACT === 'true',
IMPORT: { IMPORT: {
DATABASE: { DATABASE: {
CONNECTION: { CONNECTION: {

View File

@ -0,0 +1,149 @@
import { advancedOperatorsSearch } from './advancedOperatorsSearch';
export const findBotByTrigger = async (
botRepository: any,
settingsRepository: any,
content: string,
instanceId: string,
) => {
// Check for triggerType 'all'
const findTriggerAll = await botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (findTriggerAll) return findTriggerAll;
const findTriggerAdvanced = await botRepository.findMany({
where: {
enabled: true,
triggerType: 'advanced',
instanceId: instanceId,
},
});
for (const advanced of findTriggerAdvanced) {
if (advancedOperatorsSearch(content, advanced.triggerValue)) {
return advanced;
}
}
// Check for exact match
const findTriggerEquals = await botRepository.findFirst({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'equals',
triggerValue: content,
instanceId: instanceId,
},
});
if (findTriggerEquals) return findTriggerEquals;
// Check for regex match
const findRegex = await botRepository.findMany({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'regex',
instanceId: instanceId,
},
});
let findTriggerRegex = null;
for (const regex of findRegex) {
const regexValue = new RegExp(regex.triggerValue);
if (regexValue.test(content)) {
findTriggerRegex = regex;
break;
}
}
if (findTriggerRegex) return findTriggerRegex;
// Check for startsWith match
const findStartsWith = await botRepository.findMany({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'startsWith',
instanceId: instanceId,
},
});
let findTriggerStartsWith = null;
for (const startsWith of findStartsWith) {
if (content.startsWith(startsWith.triggerValue)) {
findTriggerStartsWith = startsWith;
break;
}
}
if (findTriggerStartsWith) return findTriggerStartsWith;
// Check for endsWith match
const findEndsWith = await botRepository.findMany({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'endsWith',
instanceId: instanceId,
},
});
let findTriggerEndsWith = null;
for (const endsWith of findEndsWith) {
if (content.endsWith(endsWith.triggerValue)) {
findTriggerEndsWith = endsWith;
break;
}
}
if (findTriggerEndsWith) return findTriggerEndsWith;
// Check for contains match
const findContains = await botRepository.findMany({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'contains',
instanceId: instanceId,
},
});
let findTriggerContains = null;
for (const contains of findContains) {
if (content.includes(contains.triggerValue)) {
findTriggerContains = contains;
break;
}
}
if (findTriggerContains) return findTriggerContains;
const fallback = await settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (fallback?.openaiIdFallback) {
const findFallback = await botRepository.findFirst({
where: {
id: fallback.openaiIdFallback,
},
});
if (findFallback) return findFallback;
}
return null;
};

View File

@ -0,0 +1,65 @@
import { configService, S3 } from '@config/env.config';
const getTypeMessage = (msg: any) => {
let mediaId: string;
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message.mediaUrl;
else mediaId = msg.key.id;
const types = {
conversation: msg?.message?.conversation,
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
contactMessage: msg?.message?.contactMessage?.displayName,
locationMessage: msg?.message?.locationMessage?.degreesLatitude,
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
// Medias
audioMessage: msg?.message?.speechToText
? msg?.message?.speechToText
: msg?.message?.audioMessage
? `audioMessage|${mediaId}`
: undefined,
imageMessage: msg?.message?.imageMessage
? `imageMessage|${mediaId}${msg?.message?.imageMessage?.caption ? `|${msg?.message?.imageMessage?.caption}` : ''}`
: undefined,
videoMessage: msg?.message?.videoMessage
? `videoMessage|${mediaId}${msg?.message?.videoMessage?.caption ? `|${msg?.message?.videoMessage?.caption}` : ''}`
: undefined,
documentMessage: msg?.message?.documentMessage
? `documentMessage|${mediaId}${
msg?.message?.documentMessage?.caption ? `|${msg?.message?.documentMessage?.caption}` : ''
}`
: undefined,
documentWithCaptionMessage: msg?.message?.documentWithCaptionMessage?.message?.documentMessage
? `documentWithCaptionMessage|${mediaId}${
msg?.message?.documentWithCaptionMessage?.message?.documentMessage?.caption
? `|${msg?.message?.documentWithCaptionMessage?.message?.documentMessage?.caption}`
: ''
}`
: undefined,
};
const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown';
return { ...types, messageType };
};
const getMessageContent = (types: any) => {
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
const result = typeKey ? types[typeKey] : undefined;
return result;
};
export const getConversationMessage = (msg: any) => {
const types = getTypeMessage(msg);
const messageContent = getMessageContent(types);
return messageContent;
};