From 887679717292c81e030e91ef406140cfcd6f07d9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 Aug 2024 09:29:49 -0300 Subject: [PATCH 1/4] refactor: openai integration --- prisma/postgresql-schema.prisma | 120 +- src/api/dto/chatbot.dto.ts | 12 + .../chatbot/chatbot.controller.ts | 113 ++ .../openai/controllers/openai.controller.ts | 1064 ++++++++++++- .../chatbot/openai/dto/openai.dto.ts | 13 - .../chatbot/openai/routes/openai.router.ts | 22 +- .../chatbot/openai/services/openai.service.ts | 1316 +---------------- .../webhook/controllers/webhook.controller.ts | 9 +- src/utils/findBotByTrigger.ts | 149 ++ src/utils/getConversationMessage.ts | 65 + 10 files changed, 1438 insertions(+), 1445 deletions(-) create mode 100644 src/api/dto/chatbot.dto.ts create mode 100644 src/utils/findBotByTrigger.ts create mode 100644 src/utils/getConversationMessage.ts diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 539453a4..0e23f0cf 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -285,29 +285,28 @@ model Websocket { } model Typebot { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - url String @db.VarChar(500) - typebot String @db.VarChar(100) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime? @updatedAt @db.Timestamp + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + description String? @db.VarChar(255) + url String @db.VarChar(500) + typebot String @db.VarChar(100) + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime? @updatedAt @db.Timestamp ignoreJids Json? triggerType TriggerType? triggerOperator TriggerOperator? triggerValue String? - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String TypebotSetting TypebotSetting[] - sessions IntegrationSession[] } model TypebotSetting { @@ -354,37 +353,36 @@ model OpenaiCreds { } model OpenaiBot { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + description String? @db.VarChar(255) botType OpenaiBotType - assistantId String? @db.VarChar(255) - functionUrl String? @db.VarChar(500) - model String? @db.VarChar(100) - systemMessages Json? @db.JsonB - assistantMessages Json? @db.JsonB - userMessages Json? @db.JsonB - maxTokens Int? @db.Integer - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer + assistantId String? @db.VarChar(255) + functionUrl String? @db.VarChar(500) + model String? @db.VarChar(100) + systemMessages Json? @db.JsonB + assistantMessages Json? @db.JsonB + userMessages Json? @db.JsonB + maxTokens Int? @db.Integer + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer ignoreJids Json? triggerType TriggerType? triggerOperator TriggerOperator? triggerValue String? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade) + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade) openaiCredsId String - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String OpenaiSetting OpenaiSetting[] - sessions IntegrationSession[] } model IntegrationSession { @@ -402,14 +400,7 @@ model IntegrationSession { instanceId String parameters Json? @db.JsonB - OpenaiBot OpenaiBot? @relation(fields: [openaiBotId], references: [id], onDelete: Cascade) - openaiBotId String? - - DifyBot Dify? @relation(fields: [difyId], references: [id], onDelete: Cascade) - difyId String? - - Typebot Typebot? @relation(fields: [typebotId], references: [id], onDelete: Cascade) - typebotId String? + botId String? } model OpenaiSetting { @@ -447,30 +438,29 @@ model Template { } model Dify { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + description String? @db.VarChar(255) botType DifyBotType - apiUrl String? @db.VarChar(255) - apiKey String? @db.VarChar(255) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer + 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) + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String DifySetting DifySetting[] - sessions IntegrationSession[] } model DifySetting { diff --git a/src/api/dto/chatbot.dto.ts b/src/api/dto/chatbot.dto.ts new file mode 100644 index 00000000..e1515483 --- /dev/null +++ b/src/api/dto/chatbot.dto.ts @@ -0,0 +1,12 @@ +export class Session { + remoteJid?: string; + sessionId?: string; + status?: string; + createdAt?: number; + updateAt?: number; +} + +export class IgnoreJidDto { + remoteJid?: string; + action?: string; +} diff --git a/src/api/integrations/chatbot/chatbot.controller.ts b/src/api/integrations/chatbot/chatbot.controller.ts index 890caf52..6c9f531b 100644 --- a/src/api/integrations/chatbot/chatbot.controller.ts +++ b/src/api/integrations/chatbot/chatbot.controller.ts @@ -2,11 +2,16 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { difyController, openaiController, typebotController, websocketController } from '@api/server.module'; import { WAMonitoringService } from '@api/services/monitor.service'; +import { Logger } from '@config/logger.config'; +import { IntegrationSession } from '@prisma/client'; +import { findBotByTrigger } from '@utils/findBotByTrigger'; export class ChatbotController { public prismaRepository: PrismaRepository; public waMonitor: WAMonitoringService; + public readonly logger = new Logger(ChatbotController.name); + constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { this.prisma = prismaRepository; this.monitor = waMonitor; @@ -63,4 +68,112 @@ export class ChatbotController { 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; + } } diff --git a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts index 70595884..c7cd2315 100644 --- a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts +++ b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts @@ -1,96 +1,930 @@ +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; -import { OpenaiCredsDto, OpenaiDto, OpenaiIgnoreJidDto } from '@api/integrations/chatbot/openai/dto/openai.dto'; +import { OpenaiCredsDto, OpenaiDto } from '@api/integrations/chatbot/openai/dto/openai.dto'; import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service'; +import { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; import { configService, Openai } from '@config/env.config'; +import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; +import { getConversationMessage } from '@utils/getConversationMessage'; +import OpenAI from 'openai'; -export class OpenaiController { - constructor(private readonly openaiService: OpenaiService) {} +import { ChatbotController } from '../../chatbot.controller'; +export class OpenaiController extends ChatbotController { + constructor( + private readonly openaiService: OpenaiService, + prismaRepository: PrismaRepository, + waMonitor: WAMonitoringService, + ) { + super(prismaRepository, waMonitor); + + this.botRepository = this.prismaRepository.openaiBot; + this.settingsRepository = this.prismaRepository.openaiSetting; + this.sessionRepository = this.prismaRepository.integrationSession; + this.credsRepository = this.prismaRepository.openaiCreds; + } + + public readonly logger = new Logger(OpenaiController.name); + + private integrationEnabled = configService.get('OPENAI').ENABLED; + private client: OpenAI; + private botRepository: any; + private settingsRepository: any; + private sessionRepository: any; + private credsRepository: any; + private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; + + // Credentials public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.createCreds(instance, data); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if (!data.apiKey) throw new Error('API Key is required'); + if (!data.name) throw new Error('Name is required'); + + try { + const creds = await this.credsRepository.create({ + data: { + name: data.name, + apiKey: data.apiKey, + instanceId: instanceId, + }, + }); + + return creds; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating openai creds'); + } } public async findOpenaiCreds(instance: InstanceDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.findCreds(instance); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const creds = await this.credsRepository.findMany({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiAssistant: true, + }, + }); + + return creds; } public async deleteCreds(instance: InstanceDto, openaiCredsId: string) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.deleteCreds(instance, openaiCredsId); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const creds = await this.credsRepository.findFirst({ + where: { + id: openaiCredsId, + }, + }); + + if (!creds) { + throw new Error('Openai Creds not found'); + } + + if (creds.instanceId !== instanceId) { + throw new Error('Openai Creds not found'); + } + + try { + await this.credsRepository.delete({ + where: { + id: openaiCredsId, + }, + }); + + return { openaiCreds: { id: openaiCredsId } }; + } catch (error) { + this.logger.error(error); + throw new Error('Error deleting openai creds'); + } } - public async createOpenai(instance: InstanceDto, data: OpenaiDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + // Models + public async getModels(instance: InstanceDto) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.create(instance, data); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if (!instanceId) throw new Error('Instance not found'); + + const defaultSettings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiCreds: true, + }, + }); + + if (!defaultSettings) throw new Error('Settings not found'); + + const { apiKey } = defaultSettings.OpenaiCreds; + + try { + this.client = new OpenAI({ apiKey }); + + const models: any = await this.client.models.list(); + + return models?.body?.data; + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching models'); + } } - public async findOpenai(instance: InstanceDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + // Bots + public async createBot(instance: InstanceDto, data: OpenaiDto) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.find(instance); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if ( + !data.openaiCredsId || + !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.openaiCredsId) data.openaiCredsId = defaultSettingCheck?.openaiCredsId || null; + if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; + if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || ''; + if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; + if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || ''; + if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; + if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; + if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; + if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; + if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; + + if (!data.openaiCredsId) { + throw new Error('Openai Creds Id is required'); + } + + if (!defaultSettingCheck) { + await this.settings(instance, { + openaiCredsId: data.openaiCredsId, + 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 openai with an "All" trigger, you cannot have more bots while it is active'); + } + + let whereDuplication: any = { + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) throw new Error('Assistant ID is required'); + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + botType: data.botType, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) throw new Error('Model is required'); + if (!data.maxTokens) throw new Error('Max tokens is required'); + + whereDuplication = { + ...whereDuplication, + model: data.model, + maxTokens: data.maxTokens, + botType: data.botType, + }; + } else { + throw new Error('Bot type is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + throw new Error('Openai 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, + 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 openaiBot = await this.botRepository.create({ + data: { + enabled: data.enabled, + description: data.description, + openaiCredsId: data.openaiCredsId, + botType: data.botType, + assistantId: data.assistantId, + functionUrl: data.functionUrl, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + 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 openaiBot; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating openai bot'); + } } - public async fetchOpenai(instance: InstanceDto, openaiBotId: string) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + public async findBot(instance: InstanceDto) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.fetch(instance, openaiBotId); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBots = await this.botRepository.findMany({ + where: { + instanceId, + }, + include: { + sessions: true, + }, + }); + + if (!openaiBots.length) { + return null; + } + + return openaiBots; } - public async updateOpenai(instance: InstanceDto, openaiBotId: string, data: OpenaiDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + public async fetchBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.update(instance, openaiBotId, data); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + include: { + sessions: true, + }, + }); + + if (!openaiBot) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai Bot not found'); + } + + return openaiBot; } - public async deleteOpenai(instance: InstanceDto, openaiBotId: string) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + public async updateBot(instance: InstanceDto, botId: string, data: OpenaiDto) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.delete(instance, openaiBotId); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!openaiBot) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai 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 openai bot with an "All" trigger, you cannot have more bots while it is active', + ); + } + } + + let whereDuplication: any = { + id: { + not: botId, + }, + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) throw new Error('Assistant ID is required'); + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) throw new Error('Model is required'); + if (!data.maxTokens) throw new Error('Max tokens is required'); + + whereDuplication = { + ...whereDuplication, + model: data.model, + maxTokens: data.maxTokens, + }; + } else { + throw new Error('Bot type is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + throw new Error('Openai 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 openaiBot = await this.botRepository.update({ + where: { + id: botId, + }, + data: { + enabled: data.enabled, + openaiCredsId: data.openaiCredsId, + botType: data.botType, + assistantId: data.assistantId, + functionUrl: data.functionUrl, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + 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 openaiBot; + } catch (error) { + this.logger.error(error); + throw new Error('Error updating openai bot'); + } } + public async deleteBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!openaiBot) { + throw new Error('Openai bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai bot not found'); + } + try { + await this.sessionRepository.deleteMany({ + where: { + botId: botId, + }, + }); + + await this.botRepository.delete({ + where: { + id: botId, + }, + }); + + return { openaiBot: { id: botId } }; + } catch (error) { + this.logger.error(error); + throw new Error('Error deleting openai bot'); + } + } + + // Settings public async settings(instance: InstanceDto, data: any) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.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: { + openaiCredsId: data.openaiCredsId, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + speechToText: data.speechToText, + openaiIdFallback: data.openaiIdFallback, + ignoreJids: data.ignoreJids, + }, + }); + + return { + openaiCredsId: updateSettings.openaiCredsId, + expire: updateSettings.expire, + keywordFinish: updateSettings.keywordFinish, + delayMessage: updateSettings.delayMessage, + unknownMessage: updateSettings.unknownMessage, + listeningFromMe: updateSettings.listeningFromMe, + stopBotFromMe: updateSettings.stopBotFromMe, + keepOpen: updateSettings.keepOpen, + debounceTime: updateSettings.debounceTime, + speechToText: updateSettings.speechToText, + openaiIdFallback: updateSettings.openaiIdFallback, + ignoreJids: updateSettings.ignoreJids, + }; + } + + const newSetttings = await this.settingsRepository.create({ + data: { + openaiCredsId: data.openaiCredsId, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + openaiIdFallback: data.openaiIdFallback, + ignoreJids: data.ignoreJids, + speechToText: data.speechToText, + instanceId: instanceId, + }, + }); + + return { + openaiCredsId: newSetttings.openaiCredsId, + expire: newSetttings.expire, + keywordFinish: newSetttings.keywordFinish, + delayMessage: newSetttings.delayMessage, + unknownMessage: newSetttings.unknownMessage, + listeningFromMe: newSetttings.listeningFromMe, + stopBotFromMe: newSetttings.stopBotFromMe, + keepOpen: newSetttings.keepOpen, + debounceTime: newSetttings.debounceTime, + openaiIdFallback: newSetttings.openaiIdFallback, + ignoreJids: newSetttings.ignoreJids, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } } public async fetchSettings(instance: InstanceDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.fetchDefaultSettings(instance); + try { + const instanceId = ( + await this.prismaRepository.instance.findFirst({ + select: { id: true }, + where: { + name: instance.instanceName, + }, + }) + )?.id; + + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + Fallback: true, + }, + }); + + if (!settings) { + return { + openaiCredsId: null, + expire: 0, + keywordFinish: '', + delayMessage: 0, + unknownMessage: '', + listeningFromMe: false, + stopBotFromMe: false, + keepOpen: false, + ignoreJids: [], + openaiIdFallback: null, + speechToText: false, + fallback: null, + }; + } + + return { + openaiCredsId: settings.openaiCredsId, + expire: settings.expire, + keywordFinish: settings.keywordFinish, + delayMessage: settings.delayMessage, + unknownMessage: settings.unknownMessage, + listeningFromMe: settings.listeningFromMe, + stopBotFromMe: settings.stopBotFromMe, + keepOpen: settings.keepOpen, + ignoreJids: settings.ignoreJids, + openaiIdFallback: settings.openaiIdFallback, + speechToText: settings.speechToText, + fallback: settings.Fallback, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching default settings'); + } } + // Sessions public async changeStatus(instance: InstanceDto, data: any) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.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 { openai: { remoteJid: remoteJid, status: status } }; + } + + if (status === 'closed') { + if (defaultSettingCheck?.keepOpen) { + await this.sessionRepository.updateMany({ + where: { + remoteJid: remoteJid, + botId: { not: null }, + status: { not: 'closed' }, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.sessionRepository.deleteMany({ + where: { + remoteJid: remoteJid, + }, + }); + } + + return { openai: { ...instance, openai: { remoteJid: remoteJid, status: status } } }; + } else { + const session = await this.sessionRepository.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + botId: { not: null }, + }, + data: { + status: status, + }, + }); + + const openaiData = { + remoteJid: remoteJid, + status: status, + session, + }; + + return { openai: { ...instance, openai: openaiData } }; + } + } catch (error) { + this.logger.error(error); + throw new Error('Error changing status'); + } } - public async fetchSessions(instance: InstanceDto, openaiBotId: string) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.fetchSessions(instance, openaiBotId); + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (openaiBot && openaiBot.instanceId !== instanceId) { + throw new Error('Openai Bot not found'); + } + + return await this.sessionRepository.findMany({ + where: { + instanceId: instanceId, + remoteJid, + botId: openaiBot ? botId : { not: null }, + }, + include: { + OpenaiBot: true, + }, + }); + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching sessions'); + } } - public async getModels(instance: InstanceDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { + if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - return this.openaiService.getModels(instance); - } - - public async ignoreJid(instance: InstanceDto, data: OpenaiIgnoreJidDto) { - if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); - - return this.openaiService.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'); + } } + // Emit public async emit({ instance, remoteJid, @@ -102,8 +936,150 @@ export class OpenaiController { msg: any; pushName?: string; }) { - if (!configService.get('OPENAI').ENABLED) return; + if (!this.integrationEnabled) return; - await this.openaiService.sendOpenai(instance, remoteJid, pushName, msg); + try { + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instance.instanceId, + }, + }); + + if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return; + + let 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; + + // verify default settings + let openaiCredsId = findBot.openaiCredsId; + 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 ( + !openaiCredsId || + !expire || + !keywordFinish || + !delayMessage || + !unknownMessage || + !listeningFromMe || + !stopBotFromMe || + !keepOpen || + !debounceTime + ) { + if (!openaiCredsId) openaiCredsId = settings.openaiCredsId; + + 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) { + session = await this.sessionRepository.update({ + where: { + id: session.id, + }, + data: { + status: 'paused', + }, + }); + } + + if (!listeningFromMe && key.fromMe) { + return; + } + + if (debounceTime && debounceTime > 0) { + this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => { + if (findBot.botType === 'assistant') { + await this.openaiService.processOpenaiAssistant( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + pushName, + key.fromMe, + findBot, + session, + settings, + debouncedContent, + ); + } + + if (findBot.botType === 'chatCompletion') { + await this.openaiService.processOpenaiChatCompletion( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + findBot, + session, + settings, + debouncedContent, + ); + } + }); + } else { + if (findBot.botType === 'assistant') { + await this.openaiService.processOpenaiAssistant( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + pushName, + key.fromMe, + findBot, + session, + settings, + content, + ); + } + + if (findBot.botType === 'chatCompletion') { + await this.openaiService.processOpenaiChatCompletion( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + findBot, + session, + settings, + content, + ); + } + } + + return; + } catch (error) { + this.logger.error(error); + return; + } } } diff --git a/src/api/integrations/chatbot/openai/dto/openai.dto.ts b/src/api/integrations/chatbot/openai/dto/openai.dto.ts index 9f0b55d3..e3c996cd 100644 --- a/src/api/integrations/chatbot/openai/dto/openai.dto.ts +++ b/src/api/integrations/chatbot/openai/dto/openai.dto.ts @@ -1,13 +1,5 @@ import { TriggerOperator, TriggerType } from '@prisma/client'; -export class Session { - remoteJid?: string; - sessionId?: string; - status?: string; - createdAt?: number; - updateAt?: number; -} - export class OpenaiCredsDto { name: string; apiKey: string; @@ -53,8 +45,3 @@ export class OpenaiSettingDto { ignoreJids?: any; speechToText?: boolean; } - -export class OpenaiIgnoreJidDto { - remoteJid?: string; - action?: string; -} diff --git a/src/api/integrations/chatbot/openai/routes/openai.router.ts b/src/api/integrations/chatbot/openai/routes/openai.router.ts index 15b468c4..ed55a4e7 100644 --- a/src/api/integrations/chatbot/openai/routes/openai.router.ts +++ b/src/api/integrations/chatbot/openai/routes/openai.router.ts @@ -1,11 +1,7 @@ import { RouterBroker } from '@api/abstract/abstract.router'; +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; -import { - OpenaiCredsDto, - OpenaiDto, - OpenaiIgnoreJidDto, - OpenaiSettingDto, -} from '@api/integrations/chatbot/openai/dto/openai.dto'; +import { OpenaiCredsDto, OpenaiDto, OpenaiSettingDto } from '@api/integrations/chatbot/openai/dto/openai.dto'; import { HttpStatus } from '@api/routes/index.router'; import { openaiController } from '@api/server.module'; import { @@ -57,7 +53,7 @@ export class OpenaiRouter extends RouterBroker { request: req, schema: openaiSchema, ClassRef: OpenaiDto, - execute: (instance, data) => openaiController.createOpenai(instance, data), + execute: (instance, data) => openaiController.createBot(instance, data), }); res.status(HttpStatus.CREATED).json(response); @@ -67,7 +63,7 @@ export class OpenaiRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => openaiController.findOpenai(instance), + execute: (instance) => openaiController.findBot(instance), }); res.status(HttpStatus.OK).json(response); @@ -77,7 +73,7 @@ export class OpenaiRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => openaiController.fetchOpenai(instance, req.params.openaiBotId), + execute: (instance) => openaiController.fetchBot(instance, req.params.openaiBotId), }); res.status(HttpStatus.OK).json(response); @@ -87,7 +83,7 @@ export class OpenaiRouter extends RouterBroker { request: req, schema: openaiSchema, 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); @@ -97,7 +93,7 @@ export class OpenaiRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => openaiController.deleteOpenai(instance, req.params.openaiBotId), + execute: (instance) => openaiController.deleteBot(instance, req.params.openaiBotId), }); res.status(HttpStatus.OK).json(response); @@ -143,10 +139,10 @@ export class OpenaiRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: openaiIgnoreJidSchema, - ClassRef: OpenaiIgnoreJidDto, + ClassRef: IgnoreJidDto, execute: (instance, data) => openaiController.ignoreJid(instance, data), }); diff --git a/src/api/integrations/chatbot/openai/services/openai.service.ts b/src/api/integrations/chatbot/openai/services/openai.service.ts index 81600154..0fe8d2b7 100644 --- a/src/api/integrations/chatbot/openai/services/openai.service.ts +++ b/src/api/integrations/chatbot/openai/services/openai.service.ts @@ -1,16 +1,9 @@ import { InstanceDto } from '@api/dto/instance.dto'; -import { - OpenaiCredsDto, - OpenaiDto, - OpenaiIgnoreJidDto, - OpenaiSettingDto, -} from '@api/integrations/chatbot/openai/dto/openai.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { ConfigService, Language, S3 } from '@config/env.config'; +import { ConfigService, Language } from '@config/env.config'; import { Logger } from '@config/logger.config'; -import { IntegrationSession, Message, OpenaiBot, OpenaiCreds, OpenaiSetting } from '@prisma/client'; -import { advancedOperatorsSearch } from '@utils/advancedOperatorsSearch'; +import { IntegrationSession, OpenaiBot, OpenaiCreds, OpenaiSetting } from '@prisma/client'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; import { downloadMediaMessage } from 'baileys'; @@ -25,1295 +18,10 @@ export class OpenaiService { private readonly prismaRepository: PrismaRepository, ) {} - private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - private client: OpenAI; private readonly logger = new Logger('OpenaiService'); - public async createCreds(instance: InstanceDto, data: OpenaiCredsDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if (!data.apiKey) throw new Error('API Key is required'); - if (!data.name) throw new Error('Name is required'); - - try { - const creds = await this.prismaRepository.openaiCreds.create({ - data: { - name: data.name, - apiKey: data.apiKey, - instanceId: instanceId, - }, - }); - - return creds; - } catch (error) { - this.logger.error(error); - throw new Error('Error creating openai creds'); - } - } - - public async findCreds(instance: InstanceDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const creds = await this.prismaRepository.openaiCreds.findMany({ - where: { - instanceId: instanceId, - }, - include: { - OpenaiAssistant: true, - }, - }); - - return creds; - } - - public async deleteCreds(instance: InstanceDto, openaiCredsId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const creds = await this.prismaRepository.openaiCreds.findFirst({ - where: { - id: openaiCredsId, - }, - }); - - if (!creds) { - throw new Error('Openai Creds not found'); - } - - if (creds.instanceId !== instanceId) { - throw new Error('Openai Creds not found'); - } - - try { - await this.prismaRepository.openaiCreds.delete({ - where: { - id: openaiCredsId, - }, - }); - - return { openaiCreds: { id: openaiCredsId } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error deleting openai creds'); - } - } - - public async create(instance: InstanceDto, data: OpenaiDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if ( - !data.openaiCredsId || - !data.expire || - !data.keywordFinish || - !data.delayMessage || - !data.unknownMessage || - !data.listeningFromMe || - !data.stopBotFromMe || - !data.keepOpen || - !data.debounceTime || - !data.ignoreJids - ) { - const defaultSettingCheck = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (!data.openaiCredsId) data.openaiCredsId = defaultSettingCheck?.openaiCredsId || null; - if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; - if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || ''; - if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; - if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || ''; - if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; - if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; - if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; - if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; - if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; - - if (!data.openaiCredsId) { - throw new Error('Openai Creds Id is required'); - } - - if (!defaultSettingCheck) { - await this.setDefaultSettings(instance, { - openaiCredsId: data.openaiCredsId, - 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.prismaRepository.openaiBot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - instanceId: instanceId, - }, - }); - - if (checkTriggerAll && data.triggerType === 'all') { - throw new Error('You already have a openai with an "All" trigger, you cannot have more bots while it is active'); - } - - let whereDuplication: any = { - instanceId: instanceId, - }; - - if (data.botType === 'assistant') { - if (!data.assistantId) throw new Error('Assistant ID is required'); - - whereDuplication = { - ...whereDuplication, - assistantId: data.assistantId, - botType: data.botType, - }; - } else if (data.botType === 'chatCompletion') { - if (!data.model) throw new Error('Model is required'); - if (!data.maxTokens) throw new Error('Max tokens is required'); - - whereDuplication = { - ...whereDuplication, - model: data.model, - maxTokens: data.maxTokens, - botType: data.botType, - }; - } else { - throw new Error('Bot type is required'); - } - - const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ - where: whereDuplication, - }); - - if (checkDuplicate) { - throw new Error('Openai 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.prismaRepository.openaiBot.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.prismaRepository.openaiBot.findFirst({ - where: { - triggerValue: data.triggerValue, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const openaiBot = await this.prismaRepository.openaiBot.create({ - data: { - enabled: data.enabled, - description: data.description, - openaiCredsId: data.openaiCredsId, - botType: data.botType, - assistantId: data.assistantId, - functionUrl: data.functionUrl, - model: data.model, - systemMessages: data.systemMessages, - assistantMessages: data.assistantMessages, - userMessages: data.userMessages, - maxTokens: data.maxTokens, - 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 openaiBot; - } catch (error) { - this.logger.error(error); - throw new Error('Error creating openai bot'); - } - } - - public async fetch(instance: InstanceDto, openaiBotId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const openaiBot = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: openaiBotId, - }, - include: { - sessions: true, - }, - }); - - if (!openaiBot) { - throw new Error('Openai Bot not found'); - } - - if (openaiBot.instanceId !== instanceId) { - throw new Error('Openai Bot not found'); - } - - return openaiBot; - } - - public async update(instance: InstanceDto, openaiBotId: string, data: OpenaiDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const openaiBot = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: openaiBotId, - }, - }); - - if (!openaiBot) { - throw new Error('Openai Bot not found'); - } - - if (openaiBot.instanceId !== instanceId) { - throw new Error('Openai Bot not found'); - } - - if (data.triggerType === 'all') { - const checkTriggerAll = await this.prismaRepository.openaiBot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - id: { - not: openaiBotId, - }, - instanceId: instanceId, - }, - }); - - if (checkTriggerAll) { - throw new Error( - 'You already have a openai bot with an "All" trigger, you cannot have more bots while it is active', - ); - } - } - - let whereDuplication: any = { - id: { - not: openaiBotId, - }, - instanceId: instanceId, - }; - - if (data.botType === 'assistant') { - if (!data.assistantId) throw new Error('Assistant ID is required'); - - whereDuplication = { - ...whereDuplication, - assistantId: data.assistantId, - }; - } else if (data.botType === 'chatCompletion') { - if (!data.model) throw new Error('Model is required'); - if (!data.maxTokens) throw new Error('Max tokens is required'); - - whereDuplication = { - ...whereDuplication, - model: data.model, - maxTokens: data.maxTokens, - }; - } else { - throw new Error('Bot type is required'); - } - - const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ - where: whereDuplication, - }); - - if (checkDuplicate) { - throw new Error('Openai 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.prismaRepository.openaiBot.findFirst({ - where: { - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - id: { not: openaiBotId }, - 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.prismaRepository.openaiBot.findFirst({ - where: { - triggerValue: data.triggerValue, - id: { not: openaiBotId }, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const openaiBot = await this.prismaRepository.openaiBot.update({ - where: { - id: openaiBotId, - }, - data: { - enabled: data.enabled, - openaiCredsId: data.openaiCredsId, - botType: data.botType, - assistantId: data.assistantId, - functionUrl: data.functionUrl, - model: data.model, - systemMessages: data.systemMessages, - assistantMessages: data.assistantMessages, - userMessages: data.userMessages, - maxTokens: data.maxTokens, - 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 openaiBot; - } catch (error) { - this.logger.error(error); - throw new Error('Error updating openai bot'); - } - } - - public async find(instance: InstanceDto): Promise { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const openaiBots = await this.prismaRepository.openaiBot.findMany({ - where: { - instanceId, - }, - include: { - sessions: true, - }, - }); - - if (!openaiBots.length) { - return null; - } - - return openaiBots; - } - - public async delete(instance: InstanceDto, openaiBotId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const openaiBot = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: openaiBotId, - }, - }); - - if (!openaiBot) { - throw new Error('Openai bot not found'); - } - - if (openaiBot.instanceId !== instanceId) { - throw new Error('Openai bot not found'); - } - try { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - openaiBotId: openaiBotId, - }, - }); - - await this.prismaRepository.openaiBot.delete({ - where: { - id: openaiBotId, - }, - }); - - return { openaiBot: { id: openaiBotId } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error deleting openai bot'); - } - } - - public async setDefaultSettings(instance: InstanceDto, data: OpenaiSettingDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (settings) { - const updateSettings = await this.prismaRepository.openaiSetting.update({ - where: { - id: settings.id, - }, - data: { - openaiCredsId: data.openaiCredsId, - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - speechToText: data.speechToText, - openaiIdFallback: data.openaiIdFallback, - ignoreJids: data.ignoreJids, - }, - }); - - return { - openaiCredsId: updateSettings.openaiCredsId, - expire: updateSettings.expire, - keywordFinish: updateSettings.keywordFinish, - delayMessage: updateSettings.delayMessage, - unknownMessage: updateSettings.unknownMessage, - listeningFromMe: updateSettings.listeningFromMe, - stopBotFromMe: updateSettings.stopBotFromMe, - keepOpen: updateSettings.keepOpen, - debounceTime: updateSettings.debounceTime, - speechToText: updateSettings.speechToText, - openaiIdFallback: updateSettings.openaiIdFallback, - ignoreJids: updateSettings.ignoreJids, - }; - } - - const newSetttings = await this.prismaRepository.openaiSetting.create({ - data: { - openaiCredsId: data.openaiCredsId, - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - openaiIdFallback: data.openaiIdFallback, - ignoreJids: data.ignoreJids, - speechToText: data.speechToText, - instanceId: instanceId, - }, - }); - - return { - openaiCredsId: newSetttings.openaiCredsId, - expire: newSetttings.expire, - keywordFinish: newSetttings.keywordFinish, - delayMessage: newSetttings.delayMessage, - unknownMessage: newSetttings.unknownMessage, - listeningFromMe: newSetttings.listeningFromMe, - stopBotFromMe: newSetttings.stopBotFromMe, - keepOpen: newSetttings.keepOpen, - debounceTime: newSetttings.debounceTime, - openaiIdFallback: newSetttings.openaiIdFallback, - ignoreJids: newSetttings.ignoreJids, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error setting default settings'); - } - } - - public async fetchDefaultSettings(instance: InstanceDto) { - try { - const instanceId = ( - await this.prismaRepository.instance.findFirst({ - select: { id: true }, - where: { - name: instance.instanceName, - }, - }) - )?.id; - - const settings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instanceId, - }, - include: { - Fallback: true, - }, - }); - - if (!settings) { - return { - openaiCredsId: null, - expire: 0, - keywordFinish: '', - delayMessage: 0, - unknownMessage: '', - listeningFromMe: false, - stopBotFromMe: false, - keepOpen: false, - ignoreJids: [], - openaiIdFallback: null, - speechToText: false, - fallback: null, - }; - } - - return { - openaiCredsId: settings.openaiCredsId, - expire: settings.expire, - keywordFinish: settings.keywordFinish, - delayMessage: settings.delayMessage, - unknownMessage: settings.unknownMessage, - listeningFromMe: settings.listeningFromMe, - stopBotFromMe: settings.stopBotFromMe, - keepOpen: settings.keepOpen, - ignoreJids: settings.ignoreJids, - openaiIdFallback: settings.openaiIdFallback, - speechToText: settings.speechToText, - fallback: settings.Fallback, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching default settings'); - } - } - - public async getModels(instance: InstanceDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if (!instanceId) throw new Error('Instance not found'); - - const defaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); - - if (!defaultSettings) throw new Error('Settings not found'); - - const { apiKey } = defaultSettings.OpenaiCreds; - - try { - this.client = new OpenAI({ apiKey }); - - const models: any = await this.client.models.list(); - - return models?.body?.data; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching models'); - } - } - - public async ignoreJid(instance: InstanceDto, data: OpenaiIgnoreJidDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.openaiSetting.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.prismaRepository.openaiSetting.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 fetchSessions(instance: InstanceDto, openaiBotId?: string, remoteJid?: string) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const openaiBot = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: openaiBotId, - }, - }); - - if (openaiBot && openaiBot.instanceId !== instanceId) { - throw new Error('Openai Bot not found'); - } - - return await this.prismaRepository.integrationSession.findMany({ - where: { - instanceId: instanceId, - remoteJid, - openaiBotId: openaiBot ? openaiBotId : { not: null }, - }, - include: { - OpenaiBot: true, - }, - }); - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching 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.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId, - }, - }); - - const remoteJid = data.remoteJid; - const status = data.status; - - if (status === 'delete') { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - openaiBotId: { not: null }, - }, - }); - - return { openai: { remoteJid: remoteJid, status: status } }; - } - - if (status === 'closed') { - if (defaultSettingCheck?.keepOpen) { - await this.prismaRepository.integrationSession.updateMany({ - where: { - remoteJid: remoteJid, - openaiBotId: { not: null }, - status: { not: 'closed' }, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - }, - }); - } - - return { openai: { ...instance, openai: { remoteJid: remoteJid, status: status } } }; - } else { - const session = await this.prismaRepository.integrationSession.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - openaiBotId: { not: null }, - }, - data: { - status: status, - }, - }); - - const openaiData = { - remoteJid: remoteJid, - status: status, - session, - }; - - return { openai: { ...instance, openai: openaiData } }; - } - } catch (error) { - this.logger.error(error); - throw new Error('Error changing status'); - } - } - - private getTypeMessage(msg: any) { - let mediaId: string; - - if (this.configService.get('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 }; - } - - private getMessageContent(types: any) { - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); - - const result = typeKey ? types[typeKey] : undefined; - - return result; - } - - private getConversationMessage(msg: any) { - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - return messageContent; - } - - public async findOpenaiByTrigger(content: string, instanceId: string) { - // Check for triggerType 'all' - const findTriggerAll = await this.prismaRepository.openaiBot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - instanceId: instanceId, - }, - }); - - if (findTriggerAll) return findTriggerAll; - - const findTriggerAdvanced = await this.prismaRepository.openaiBot.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 this.prismaRepository.openaiBot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'equals', - triggerValue: content, - instanceId: instanceId, - }, - }); - - if (findTriggerEquals) return findTriggerEquals; - - // Check for regex match - const findRegex = await this.prismaRepository.openaiBot.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 this.prismaRepository.openaiBot.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 this.prismaRepository.openaiBot.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 this.prismaRepository.openaiBot.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 this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (fallback?.openaiIdFallback) { - const findFallback = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: fallback.openaiIdFallback, - }, - }); - - if (findFallback) return findFallback; - } - - return null; - } - - private processDebounce(content: string, remoteJid: string, debounceTime: number, callback: any) { - if (this.userMessageDebounce[remoteJid]) { - this.userMessageDebounce[remoteJid].message += ` ${content}`; - this.logger.log('message debounced: ' + this.userMessageDebounce[remoteJid].message); - clearTimeout(this.userMessageDebounce[remoteJid].timeoutId); - } else { - this.userMessageDebounce[remoteJid] = { - message: content, - timeoutId: null, - }; - } - - this.userMessageDebounce[remoteJid].timeoutId = setTimeout(() => { - const myQuestion = this.userMessageDebounce[remoteJid].message; - this.logger.log('Debounce complete. Processing message: ' + myQuestion); - - delete this.userMessageDebounce[remoteJid]; - callback(myQuestion); - }, debounceTime * 1000); - } - - public async sendOpenai(instance: InstanceDto, remoteJid: string, pushName: string, msg: Message) { - try { - const settings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (settings?.ignoreJids) { - const ignoreJids: any = settings.ignoreJids; - - 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; - } - - if (ignoreContacts && remoteJid.endsWith('@s.whatsapp.net')) { - this.logger.warn('Ignoring message from contact: ' + remoteJid); - return; - } - - if (ignoreJids.includes(remoteJid)) { - this.logger.warn('Ignoring message from jid: ' + remoteJid); - return; - } - } - - let session = await this.prismaRepository.integrationSession.findFirst({ - where: { - remoteJid: remoteJid, - instanceId: instance.instanceId, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (session) { - if (session.status !== 'closed' && !session.openaiBotId) { - this.logger.warn('Session is already opened in another integration'); - return; - } else if (!session.openaiBotId) { - session = null; - } - } - - const content = this.getConversationMessage(msg); - - let findOpenai = null; - - if (!session) { - findOpenai = await this.findOpenaiByTrigger(content, instance.instanceId); - - if (!findOpenai) { - return; - } - } else { - findOpenai = await this.prismaRepository.openaiBot.findFirst({ - where: { - id: session.openaiBotId, - }, - }); - } - - if (!findOpenai) return; - - let openaiCredsId = findOpenai.openaiCredsId; - let expire = findOpenai.expire; - let keywordFinish = findOpenai.keywordFinish; - let delayMessage = findOpenai.delayMessage; - let unknownMessage = findOpenai.unknownMessage; - let listeningFromMe = findOpenai.listeningFromMe; - let stopBotFromMe = findOpenai.stopBotFromMe; - let keepOpen = findOpenai.keepOpen; - let debounceTime = findOpenai.debounceTime; - - if ( - !openaiCredsId || - !expire || - !keywordFinish || - !delayMessage || - !unknownMessage || - !listeningFromMe || - !stopBotFromMe || - !keepOpen || - !debounceTime - ) { - if (!openaiCredsId) openaiCredsId = settings.openaiCredsId; - - 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) { - session = await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'paused', - }, - }); - } - - if (!listeningFromMe && key.fromMe) { - return; - } - - if (debounceTime && debounceTime > 0) { - this.processDebounce(content, remoteJid, debounceTime, async (debouncedContent) => { - if (findOpenai.botType === 'assistant') { - await this.processOpenaiAssistant( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - pushName, - key.fromMe, - findOpenai, - session, - settings, - debouncedContent, - ); - } - - if (findOpenai.botType === 'chatCompletion') { - await this.processOpenaiChatCompletion( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - findOpenai, - session, - settings, - debouncedContent, - ); - } - }); - } else { - if (findOpenai.botType === 'assistant') { - await this.processOpenaiAssistant( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - pushName, - key.fromMe, - findOpenai, - session, - settings, - content, - ); - } - - if (findOpenai.botType === 'chatCompletion') { - await this.processOpenaiChatCompletion( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - findOpenai, - session, - settings, - content, - ); - } - } - - return; - } catch (error) { - this.logger.error(error); - return; - } - } - public async createAssistantNewSession(instance: InstanceDto, data: any) { if (data.remoteJid === 'status@broadcast') return; @@ -1340,7 +48,7 @@ export class OpenaiService { sessionId: threadId, status: 'opened', awaitUser: false, - openaiBotId: data.openaiBotId, + botId: data.botId, instanceId: instance.instanceId, }, }); @@ -1365,7 +73,7 @@ export class OpenaiService { const data = await this.createAssistantNewSession(instance, { remoteJid, openaiCredsId: openaiBot.openaiCredsId, - openaiBotId: openaiBot.id, + botId: openaiBot.id, }); if (data.session) { @@ -1564,7 +272,7 @@ export class OpenaiService { return content.includes('imageMessage'); } - private async processOpenaiAssistant( + public async processOpenaiAssistant( instance: any, remoteJid: string, pushName: string, @@ -1600,7 +308,7 @@ export class OpenaiService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - openaiBotId: openaiBot.id, + botId: openaiBot.id, remoteJid: remoteJid, }, }); @@ -1665,7 +373,7 @@ export class OpenaiService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - openaiBotId: openaiBot.id, + botId: openaiBot.id, remoteJid: remoteJid, }, }); @@ -1814,7 +522,7 @@ export class OpenaiService { sessionId: id, status: 'opened', awaitUser: false, - openaiBotId: data.openaiBotId, + botId: data.botId, instanceId: instance.instanceId, }, }); @@ -1837,7 +545,7 @@ export class OpenaiService { const data = await this.createChatCompletionNewSession(instance, { remoteJid, openaiCredsId: openaiBot.openaiCredsId, - openaiBotId: openaiBot.id, + botId: openaiBot.id, }); session = data.session; @@ -1972,7 +680,7 @@ export class OpenaiService { return; } - private async processOpenaiChatCompletion( + public async processOpenaiChatCompletion( instance: any, remoteJid: string, openaiBot: OpenaiBot, @@ -2006,7 +714,7 @@ export class OpenaiService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - openaiBotId: openaiBot.id, + botId: openaiBot.id, remoteJid: remoteJid, }, }); @@ -2061,7 +769,7 @@ export class OpenaiService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - openaiBotId: openaiBot.id, + botId: openaiBot.id, remoteJid: remoteJid, }, }); diff --git a/src/api/integrations/event/webhook/controllers/webhook.controller.ts b/src/api/integrations/event/webhook/controllers/webhook.controller.ts index 5f924c6b..fefe00b6 100644 --- a/src/api/integrations/event/webhook/controllers/webhook.controller.ts +++ b/src/api/integrations/event/webhook/controllers/webhook.controller.ts @@ -1,7 +1,7 @@ import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; 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 { BadRequestException, NotFoundException } from '@exceptions'; import axios from 'axios'; @@ -31,7 +31,7 @@ export class WebhookController extends EventController { } await this.get(instanceName); - + return this.prisma.webhook.upsert({ where: { instanceId: this.monitor.waInstances[instanceName].instanceId, @@ -89,10 +89,6 @@ export class WebhookController extends EventController { apiKey?: string; local?: boolean; }): Promise { - if (!configService.get('WEBSOCKET')?.ENABLED) { - return; - } - const instanceWebhook = await this.get(instanceName); const webhookGlobal = configService.get('WEBHOOK'); const webhookLocal = instanceWebhook?.events; @@ -110,6 +106,7 @@ export class WebhookController extends EventController { server_url: serverUrl, apikey: apiKey, }; + if (local) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { let baseURL: string; diff --git a/src/utils/findBotByTrigger.ts b/src/utils/findBotByTrigger.ts new file mode 100644 index 00000000..dc199254 --- /dev/null +++ b/src/utils/findBotByTrigger.ts @@ -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; +}; diff --git a/src/utils/getConversationMessage.ts b/src/utils/getConversationMessage.ts new file mode 100644 index 00000000..fb08d9ea --- /dev/null +++ b/src/utils/getConversationMessage.ts @@ -0,0 +1,65 @@ +import { configService, S3 } from '@config/env.config'; + +const getTypeMessage = (msg: any) => { + let mediaId: string; + + if (configService.get('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; +}; From edeb1efd2a99d6bc664ab19b2388d87350da28e2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 Aug 2024 13:37:22 -0300 Subject: [PATCH 2/4] refactor: chatbot integration --- .../baileys/whatsapp.baileys.service.ts | 37 +- .../business/whatsapp.business.service.ts | 37 +- .../chatbot/chatbot.controller.ts | 30 + .../dify/controllers/dify.controller.ts | 825 +++++++++- .../integrations/chatbot/dify/dto/dify.dto.ts | 13 - .../chatbot/dify/routes/dify.router.ts | 17 +- .../chatbot/dify/services/dify.service.ts | 1073 +------------ .../openai/controllers/openai.controller.ts | 62 +- .../typebot/controllers/typebot.controller.ts | 1102 +++++++++++++- .../chatbot/typebot/dto/typebot.dto.ts | 14 - .../chatbot/typebot/routes/typebot.router.ts | 21 +- .../typebot/services/typebot.service.ts | 1347 +---------------- src/api/server.module.ts | 6 +- 13 files changed, 1957 insertions(+), 2627 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts index 533045b6..f1c0378a 100644 --- a/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts @@ -60,13 +60,11 @@ import { configService, ConfigSessionPhone, Database, - Dify, Log, Openai, ProviderSession, QrCode, S3, - Typebot, } from '@config/env.config'; import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions'; import ffmpegPath from '@ffmpeg-installer/ffmpeg'; @@ -2012,35 +2010,12 @@ export class BaileysStartupService extends ChannelStartupService { ); } - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration) { - if (this.configService.get('TYPEBOT').ENABLED) { - if (messageRaw.messageType !== 'reactionMessage') - await this.typebotService.sendTypebot( - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - - if (this.configService.get('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').ENABLED) { - if (messageRaw.messageType !== 'reactionMessage') - await this.difyService.sendDify( - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw.key.remoteJid, - 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('DATABASE').SAVE_DATA.NEW_MESSAGE) await this.prismaRepository.message.create({ diff --git a/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts b/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts index 2207ff29..f6b4b56e 100644 --- a/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts +++ b/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts @@ -20,7 +20,7 @@ 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, 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 axios from 'axios'; import { arrayUnique, isURL } from 'class-validator'; @@ -923,35 +923,12 @@ export class BusinessStartupService extends ChannelStartupService { ); } - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration) { - if (this.configService.get('TYPEBOT').ENABLED) { - if (messageRaw.messageType !== 'reactionMessage') - await this.typebotService.sendTypebot( - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - - if (this.configService.get('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').ENABLED) { - if (messageRaw.messageType !== 'reactionMessage') - await this.difyService.sendDify( - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - } + 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, diff --git a/src/api/integrations/chatbot/chatbot.controller.ts b/src/api/integrations/chatbot/chatbot.controller.ts index 6c9f531b..7054f4f9 100644 --- a/src/api/integrations/chatbot/chatbot.controller.ts +++ b/src/api/integrations/chatbot/chatbot.controller.ts @@ -6,6 +6,36 @@ 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; + findBot(instance: InstanceDto): Promise; + fetchBot(instance: InstanceDto, botId: string): Promise; + updateBot(instance: InstanceDto, botId: string, data: any): Promise; + deleteBot(instance: InstanceDto, botId: string): Promise; + + settings(instance: InstanceDto, data: any): Promise; + fetchSettings(instance: InstanceDto): Promise; + + changeStatus(instance: InstanceDto, botId: string, status: string): Promise; + fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string): Promise; + ignoreJid(instance: InstanceDto, data: any): Promise; + + emit(data: EmitData): Promise; +} + export class ChatbotController { public prismaRepository: PrismaRepository; public waMonitor: WAMonitoringService; diff --git a/src/api/integrations/chatbot/dify/controllers/dify.controller.ts b/src/api/integrations/chatbot/dify/controllers/dify.controller.ts index 79972473..5add7b65 100644 --- a/src/api/integrations/chatbot/dify/controllers/dify.controller.ts +++ b/src/api/integrations/chatbot/dify/controllers/dify.controller.ts @@ -1,84 +1,825 @@ +import { IgnoreJidDto } from '@api/dto/chatbot.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 { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; import { configService, Dify } from '@config/env.config'; +import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; +import { getConversationMessage } from '@utils/getConversationMessage'; -export class DifyController { - constructor(private readonly difyService: DifyService) {} +import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; - public async createDify(instance: InstanceDto, data: DifyDto) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); +export class DifyController extends ChatbotController implements ChatbotControllerInterface { + 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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public readonly logger = new Logger(DifyController.name); - return this.difyService.find(instance); + integrationEnabled = configService.get('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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public async findBot(instance: InstanceDto) { + 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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public async fetchBot(instance: InstanceDto, botId: string) { + 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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) { + 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) { - if (!configService.get('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) { - if (!configService.get('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) { - if (!configService.get('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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) { + 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) { - if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { + 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({ - instance, - remoteJid, - msg, - }: { - instance: InstanceDto; - remoteJid: string; - msg: any; - pushName?: string; - }) { - if (!configService.get('DIFY').ENABLED) return; + // Emit + public async emit({ instance, remoteJid, msg }: EmitData) { + if (!this.integrationEnabled) return; - await this.difyService.sendDify(instance, remoteJid, msg); + try { + const settings = await this.prismaRepository.difySetting.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; + } } } diff --git a/src/api/integrations/chatbot/dify/dto/dify.dto.ts b/src/api/integrations/chatbot/dify/dto/dify.dto.ts index 7967f491..734e7dd5 100644 --- a/src/api/integrations/chatbot/dify/dto/dify.dto.ts +++ b/src/api/integrations/chatbot/dify/dto/dify.dto.ts @@ -1,13 +1,5 @@ import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; -export class Session { - remoteJid?: string; - sessionId?: string; - status?: string; - createdAt?: number; - updateAt?: number; -} - export class DifyDto { enabled?: boolean; description?: string; @@ -40,8 +32,3 @@ export class DifySettingDto { difyIdFallback?: string; ignoreJids?: any; } - -export class DifyIgnoreJidDto { - remoteJid?: string; - action?: string; -} diff --git a/src/api/integrations/chatbot/dify/routes/dify.router.ts b/src/api/integrations/chatbot/dify/routes/dify.router.ts index 017c6171..1d80b903 100644 --- a/src/api/integrations/chatbot/dify/routes/dify.router.ts +++ b/src/api/integrations/chatbot/dify/routes/dify.router.ts @@ -1,6 +1,7 @@ import { RouterBroker } from '@api/abstract/abstract.router'; +import { IgnoreJidDto } from '@api/dto/chatbot.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 { difyController } from '@api/server.module'; import { @@ -21,7 +22,7 @@ export class DifyRouter extends RouterBroker { request: req, schema: difySchema, ClassRef: DifyDto, - execute: (instance, data) => difyController.createDify(instance, data), + execute: (instance, data) => difyController.createBot(instance, data), }); res.status(HttpStatus.CREATED).json(response); @@ -31,7 +32,7 @@ export class DifyRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => difyController.findDify(instance), + execute: (instance) => difyController.findBot(instance), }); res.status(HttpStatus.OK).json(response); @@ -41,7 +42,7 @@ export class DifyRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => difyController.fetchDify(instance, req.params.difyId), + execute: (instance) => difyController.fetchBot(instance, req.params.difyId), }); res.status(HttpStatus.OK).json(response); @@ -51,7 +52,7 @@ export class DifyRouter extends RouterBroker { request: req, schema: difySchema, 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); @@ -61,7 +62,7 @@ export class DifyRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => difyController.deleteDify(instance, req.params.difyId), + execute: (instance) => difyController.deleteBot(instance, req.params.difyId), }); res.status(HttpStatus.OK).json(response); @@ -107,10 +108,10 @@ export class DifyRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: difyIgnoreJidSchema, - ClassRef: DifyIgnoreJidDto, + ClassRef: IgnoreJidDto, execute: (instance, data) => difyController.ignoreJid(instance, data), }); diff --git a/src/api/integrations/chatbot/dify/services/dify.service.ts b/src/api/integrations/chatbot/dify/services/dify.service.ts index a1fbf21a..4a2aea07 100644 --- a/src/api/integrations/chatbot/dify/services/dify.service.ts +++ b/src/api/integrations/chatbot/dify/services/dify.service.ts @@ -1,11 +1,9 @@ import { InstanceDto } from '@api/dto/instance.dto'; -import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '@api/integrations/chatbot/dify/dto/dify.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { Auth, ConfigService, HttpServer, S3 } from '@config/env.config'; +import { Auth, ConfigService, HttpServer } from '@config/env.config'; import { Logger } from '@config/logger.config'; -import { Dify, DifySetting, IntegrationSession, Message } from '@prisma/client'; -import { advancedOperatorsSearch } from '@utils/advancedOperatorsSearch'; +import { Dify, DifySetting, IntegrationSession } from '@prisma/client'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; import { Readable } from 'stream'; @@ -17,1067 +15,8 @@ export class DifyService { private readonly prismaRepository: PrismaRepository, ) {} - private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - private readonly logger = new Logger('DifyService'); - public async create(instance: InstanceDto, data: DifyDto) { - 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.prismaRepository.difySetting.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.setDefaultSettings(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.prismaRepository.dify.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.prismaRepository.dify.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.prismaRepository.dify.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.prismaRepository.dify.findFirst({ - where: { - triggerValue: data.triggerValue, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const dify = await this.prismaRepository.dify.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 dify; - } catch (error) { - this.logger.error(error); - throw new Error('Error creating dify'); - } - } - - public async fetch(instance: InstanceDto, difyId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const dify = await this.prismaRepository.dify.findFirst({ - where: { - id: difyId, - }, - include: { - sessions: true, - }, - }); - - if (!dify) { - throw new Error('Dify not found'); - } - - if (dify.instanceId !== instanceId) { - throw new Error('Dify not found'); - } - - return dify; - } - - public async update(instance: InstanceDto, difyId: string, data: DifyDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const dify = await this.prismaRepository.dify.findFirst({ - where: { - id: difyId, - }, - }); - - if (!dify) { - throw new Error('Dify not found'); - } - - if (dify.instanceId !== instanceId) { - throw new Error('Dify not found'); - } - - if (data.triggerType === 'all') { - const checkTriggerAll = await this.prismaRepository.dify.findFirst({ - where: { - enabled: true, - triggerType: 'all', - id: { - not: difyId, - }, - 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.prismaRepository.dify.findFirst({ - where: { - id: { - not: difyId, - }, - 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.prismaRepository.dify.findFirst({ - where: { - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - id: { not: difyId }, - 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.prismaRepository.dify.findFirst({ - where: { - triggerValue: data.triggerValue, - id: { not: difyId }, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const dify = await this.prismaRepository.dify.update({ - where: { - id: difyId, - }, - 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 dify; - } catch (error) { - this.logger.error(error); - throw new Error('Error updating dify'); - } - } - - public async find(instance: InstanceDto): Promise { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const difys = await this.prismaRepository.dify.findMany({ - where: { - instanceId: instanceId, - }, - include: { - sessions: true, - }, - }); - - if (!difys.length) { - return null; - } - - return difys; - } - - public async delete(instance: InstanceDto, difyId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const dify = await this.prismaRepository.dify.findFirst({ - where: { - id: difyId, - }, - }); - - if (!dify) { - throw new Error('Dify not found'); - } - - if (dify.instanceId !== instanceId) { - throw new Error('Dify not found'); - } - try { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - difyId: difyId, - }, - }); - - await this.prismaRepository.dify.delete({ - where: { - id: difyId, - }, - }); - - return { dify: { id: difyId } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error deleting openai bot'); - } - } - - public async setDefaultSettings(instance: InstanceDto, data: DifySettingDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.difySetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (settings) { - const updateSettings = await this.prismaRepository.difySetting.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.prismaRepository.difySetting.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 fetchDefaultSettings(instance: InstanceDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.difySetting.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'); - } - } - - public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.difySetting.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.prismaRepository.difySetting.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 fetchSessions(instance: InstanceDto, difyId?: string, remoteJid?: string) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const dify = await this.prismaRepository.dify.findFirst({ - where: { - id: difyId, - }, - }); - - if (dify && dify.instanceId !== instanceId) { - throw new Error('Dify not found'); - } - - return await this.prismaRepository.integrationSession.findMany({ - where: { - instanceId: instanceId, - remoteJid, - difyId: dify ? difyId : { not: null }, - }, - include: { - DifyBot: true, - }, - }); - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching 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.prismaRepository.difySetting.findFirst({ - where: { - instanceId, - }, - }); - - const remoteJid = data.remoteJid; - const status = data.status; - - if (status === 'delete') { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - difyId: { not: null }, - }, - }); - - return { dify: { remoteJid: remoteJid, status: status } }; - } - - if (status === 'closed') { - if (defaultSettingCheck?.keepOpen) { - await this.prismaRepository.integrationSession.updateMany({ - where: { - remoteJid: remoteJid, - difyId: { not: null }, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - difyId: { not: null }, - }, - }); - } - - return { dify: { ...instance, dify: { remoteJid: remoteJid, status: status } } }; - } else { - const session = await this.prismaRepository.integrationSession.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - difyId: { not: null }, - }, - data: { - status: status, - }, - }); - - const difyData = { - remoteJid: remoteJid, - status: status, - session, - }; - - return { dify: { ...instance, dify: difyData } }; - } - } catch (error) { - this.logger.error(error); - throw new Error('Error changing status'); - } - } - - private getTypeMessage(msg: any) { - let mediaId: string; - - if (this.configService.get('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 }; - } - - private getMessageContent(types: any) { - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); - - const result = typeKey ? types[typeKey] : undefined; - - return result; - } - - private getConversationMessage(msg: any) { - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - return messageContent; - } - - public async findDifyByTrigger(content: string, instanceId: string) { - // Check for triggerType 'all' - const findTriggerAll = await this.prismaRepository.dify.findFirst({ - where: { - enabled: true, - triggerType: 'all', - instanceId: instanceId, - }, - }); - - if (findTriggerAll) return findTriggerAll; - - const findTriggerAdvanced = await this.prismaRepository.dify.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 this.prismaRepository.dify.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'equals', - triggerValue: content, - instanceId: instanceId, - }, - }); - - if (findTriggerEquals) return findTriggerEquals; - - // Check for regex match - const findRegex = await this.prismaRepository.dify.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 this.prismaRepository.dify.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 this.prismaRepository.dify.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 this.prismaRepository.dify.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 this.prismaRepository.difySetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (fallback?.difyIdFallback) { - const findFallback = await this.prismaRepository.dify.findFirst({ - where: { - id: fallback.difyIdFallback, - }, - }); - - if (findFallback) return findFallback; - } - - return null; - } - - private processDebounce(content: string, remoteJid: string, debounceTime: number, callback: any) { - if (this.userMessageDebounce[remoteJid]) { - this.userMessageDebounce[remoteJid].message += ` ${content}`; - this.logger.log('message debounced: ' + this.userMessageDebounce[remoteJid].message); - clearTimeout(this.userMessageDebounce[remoteJid].timeoutId); - } else { - this.userMessageDebounce[remoteJid] = { - message: content, - timeoutId: null, - }; - } - - this.userMessageDebounce[remoteJid].timeoutId = setTimeout(() => { - const myQuestion = this.userMessageDebounce[remoteJid].message; - this.logger.log('Debounce complete. Processing message: ' + myQuestion); - - delete this.userMessageDebounce[remoteJid]; - callback(myQuestion); - }, debounceTime * 1000); - } - - public async sendDify(instance: InstanceDto, remoteJid: string, msg: Message) { - try { - const settings = await this.prismaRepository.difySetting.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (settings?.ignoreJids) { - const ignoreJids: any = settings.ignoreJids; - - 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; - } - - if (ignoreContacts && remoteJid.endsWith('@s.whatsapp.net')) { - this.logger.warn('Ignoring message from contact: ' + remoteJid); - return; - } - - if (ignoreJids.includes(remoteJid)) { - this.logger.warn('Ignoring message from jid: ' + remoteJid); - return; - } - } - - let session = await this.prismaRepository.integrationSession.findFirst({ - where: { - remoteJid: remoteJid, - instanceId: instance.instanceId, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (session) { - if (session.status !== 'closed' && !session.difyId) { - this.logger.warn('Session is already opened in another integration'); - return; - } else if (!session.difyId) { - session = null; - } - } - - const content = this.getConversationMessage(msg); - - let findDify = null; - - if (!session) { - findDify = await this.findDifyByTrigger(content, instance.instanceId); - - if (!findDify) { - return; - } - } else { - findDify = await this.prismaRepository.dify.findFirst({ - where: { - id: session.difyId, - }, - }); - } - - if (!findDify) return; - - let expire = findDify.expire; - let keywordFinish = findDify.keywordFinish; - let delayMessage = findDify.delayMessage; - let unknownMessage = findDify.unknownMessage; - let listeningFromMe = findDify.listeningFromMe; - let stopBotFromMe = findDify.stopBotFromMe; - let keepOpen = findDify.keepOpen; - let debounceTime = findDify.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(content, remoteJid, debounceTime, async (debouncedContent) => { - await this.processDify( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - findDify, - session, - settings, - debouncedContent, - msg?.pushName, - ); - }); - } else { - await this.processDify( - this.waMonitor.waInstances[instance.instanceName], - remoteJid, - findDify, - session, - settings, - content, - msg?.pushName, - ); - } - - return; - } catch (error) { - this.logger.error(error); - return; - } - } - public async createNewSession(instance: InstanceDto, data: any) { try { const session = await this.prismaRepository.integrationSession.create({ @@ -1086,7 +25,7 @@ export class DifyService { sessionId: data.remoteJid, status: 'opened', awaitUser: false, - difyId: data.difyId, + botId: data.difyId, instanceId: instance.instanceId, }, }); @@ -1575,7 +514,7 @@ export class DifyService { return; } - private async processDify( + public async processDify( instance: any, remoteJid: string, dify: Dify, @@ -1610,7 +549,7 @@ export class DifyService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - difyId: dify.id, + botId: dify.id, remoteJid: remoteJid, }, }); @@ -1665,7 +604,7 @@ export class DifyService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - difyId: dify.id, + botId: dify.id, remoteJid: remoteJid, }, }); diff --git a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts index c7cd2315..6e54618a 100644 --- a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts +++ b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts @@ -10,9 +10,9 @@ import { BadRequestException } from '@exceptions'; import { getConversationMessage } from '@utils/getConversationMessage'; import OpenAI from 'openai'; -import { ChatbotController } from '../../chatbot.controller'; +import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; -export class OpenaiController extends ChatbotController { +export class OpenaiController extends ChatbotController implements ChatbotControllerInterface { constructor( private readonly openaiService: OpenaiService, prismaRepository: PrismaRepository, @@ -28,13 +28,13 @@ export class OpenaiController extends ChatbotController { public readonly logger = new Logger(OpenaiController.name); - private integrationEnabled = configService.get('OPENAI').ENABLED; + integrationEnabled = configService.get('OPENAI').ENABLED; + botRepository: any; + settingsRepository: any; + sessionRepository: any; + userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; private client: OpenAI; - private botRepository: any; - private settingsRepository: any; - private sessionRepository: any; private credsRepository: any; - private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; // Credentials public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) { @@ -311,7 +311,7 @@ export class OpenaiController extends ChatbotController { } try { - const openaiBot = await this.botRepository.create({ + const bot = await this.botRepository.create({ data: { enabled: data.enabled, description: data.description, @@ -340,7 +340,7 @@ export class OpenaiController extends ChatbotController { }, }); - return openaiBot; + return bot; } catch (error) { this.logger.error(error); throw new Error('Error creating openai bot'); @@ -358,7 +358,7 @@ export class OpenaiController extends ChatbotController { }) .then((instance) => instance.id); - const openaiBots = await this.botRepository.findMany({ + const bots = await this.botRepository.findMany({ where: { instanceId, }, @@ -367,11 +367,11 @@ export class OpenaiController extends ChatbotController { }, }); - if (!openaiBots.length) { + if (!bots.length) { return null; } - return openaiBots; + return bots; } public async fetchBot(instance: InstanceDto, botId: string) { @@ -385,7 +385,7 @@ export class OpenaiController extends ChatbotController { }) .then((instance) => instance.id); - const openaiBot = await this.botRepository.findFirst({ + const bot = await this.botRepository.findFirst({ where: { id: botId, }, @@ -394,15 +394,15 @@ export class OpenaiController extends ChatbotController { }, }); - if (!openaiBot) { + if (!bot) { throw new Error('Openai Bot not found'); } - if (openaiBot.instanceId !== instanceId) { + if (bot.instanceId !== instanceId) { throw new Error('Openai Bot not found'); } - return openaiBot; + return bot; } public async updateBot(instance: InstanceDto, botId: string, data: OpenaiDto) { @@ -416,17 +416,17 @@ export class OpenaiController extends ChatbotController { }) .then((instance) => instance.id); - const openaiBot = await this.botRepository.findFirst({ + const bot = await this.botRepository.findFirst({ where: { id: botId, }, }); - if (!openaiBot) { + if (!bot) { throw new Error('Openai Bot not found'); } - if (openaiBot.instanceId !== instanceId) { + if (bot.instanceId !== instanceId) { throw new Error('Openai Bot not found'); } @@ -522,7 +522,7 @@ export class OpenaiController extends ChatbotController { } try { - const openaiBot = await this.botRepository.update({ + const bot = await this.botRepository.update({ where: { id: botId, }, @@ -553,7 +553,7 @@ export class OpenaiController extends ChatbotController { }, }); - return openaiBot; + return bot; } catch (error) { this.logger.error(error); throw new Error('Error updating openai bot'); @@ -571,17 +571,17 @@ export class OpenaiController extends ChatbotController { }) .then((instance) => instance.id); - const openaiBot = await this.botRepository.findFirst({ + const bot = await this.botRepository.findFirst({ where: { id: botId, }, }); - if (!openaiBot) { + if (!bot) { throw new Error('Openai bot not found'); } - if (openaiBot.instanceId !== instanceId) { + if (bot.instanceId !== instanceId) { throw new Error('Openai bot not found'); } try { @@ -597,7 +597,7 @@ export class OpenaiController extends ChatbotController { }, }); - return { openaiBot: { id: botId } }; + return { bot: { id: botId } }; } catch (error) { this.logger.error(error); throw new Error('Error deleting openai bot'); @@ -925,17 +925,7 @@ export class OpenaiController extends ChatbotController { } // Emit - public async emit({ - instance, - remoteJid, - msg, - pushName, - }: { - instance: InstanceDto; - remoteJid: string; - msg: any; - pushName?: string; - }) { + public async emit({ instance, remoteJid, msg, pushName }: EmitData) { if (!this.integrationEnabled) return; try { diff --git a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts index 79827cfc..01db49dd 100644 --- a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts +++ b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts @@ -1,76 +1,971 @@ +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; -import { TypebotDto, TypebotIgnoreJidDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; +import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service'; -import { configService, Typebot } from '@config/env.config'; +import { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; +import { Events } from '@api/types/wa.types'; +import { Auth, configService, HttpServer, Typebot } from '@config/env.config'; +import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; +import { getConversationMessage } from '@utils/getConversationMessage'; +import axios from 'axios'; -export class TypebotController { - constructor(private readonly typebotService: TypebotService) {} +import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller'; - public async createTypebot(instance: InstanceDto, data: TypebotDto) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); +export class TypebotController extends ChatbotController implements ChatbotControllerInterface { + constructor( + private readonly typebotService: TypebotService, + prismaRepository: PrismaRepository, + waMonitor: WAMonitoringService, + ) { + super(prismaRepository, waMonitor); - return this.typebotService.create(instance, data); + this.botRepository = this.prismaRepository.typebot; + this.settingsRepository = this.prismaRepository.typebotSetting; + this.sessionRepository = this.prismaRepository.integrationSession; } - public async findTypebot(instance: InstanceDto) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public readonly logger = new Logger(TypebotController.name); - return this.typebotService.find(instance); + integrationEnabled = configService.get('TYPEBOT').ENABLED; + botRepository: any; + settingsRepository: any; + sessionRepository: any; + userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; + + // Bots + public async createBot(instance: InstanceDto, data: TypebotDto) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); + + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if ( + !data.expire || + !data.keywordFinish || + !data.delayMessage || + !data.unknownMessage || + !data.listeningFromMe || + !data.stopBotFromMe || + !data.keepOpen || + !data.debounceTime || + !data.ignoreJids + ) { + const defaultSettingCheck = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; + if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; + if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; + if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; + if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; + if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; + if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; + if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; + if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; + + if (!defaultSettingCheck) { + await this.settings(instance, { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + ignoreJids: data.ignoreJids, + }); + } + } + + const checkTriggerAll = await this.botRepository.findFirst({ + where: { + enabled: true, + triggerType: 'all', + instanceId: instanceId, + }, + }); + + if (checkTriggerAll && data.triggerType === 'all') { + throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + url: data.url, + typebot: data.typebot, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Typebot already exists'); + } + + if (data.triggerType === 'keyword') { + if (!data.triggerOperator || !data.triggerValue) { + throw new Error('Trigger operator and value are required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + } + + if (data.triggerType === 'advanced') { + if (!data.triggerValue) { + throw new Error('Trigger value is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerValue: data.triggerValue, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + } + + try { + const bot = await this.botRepository.create({ + data: { + enabled: data.enabled, + description: data.description, + url: data.url, + typebot: data.typebot, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + instanceId: instanceId, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + }, + }); + + return bot; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating typebot'); + } } - public async fetchTypebot(instance: InstanceDto, typebotId: string) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async findBot(instance: InstanceDto) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.fetch(instance, typebotId); + 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 updateTypebot(instance: InstanceDto, typebotId: string, data: TypebotDto) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async fetchBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.update(instance, typebotId, 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('Typebot not found'); + } + + if (bot.instanceId !== instanceId) { + throw new Error('Typebot not found'); + } + + return bot; } - public async deleteTypebot(instance: InstanceDto, typebotId: string) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.delete(instance, typebotId); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const typebot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!typebot) { + throw new Error('Typebot not found'); + } + + if (typebot.instanceId !== instanceId) { + throw new Error('Typebot not found'); + } + + if (data.triggerType === 'all') { + const checkTriggerAll = await this.botRepository.findFirst({ + where: { + enabled: true, + triggerType: 'all', + id: { + not: botId, + }, + instanceId: instanceId, + }, + }); + + if (checkTriggerAll) { + throw new Error( + 'You already have a typebot with an "All" trigger, you cannot have more bots while it is active', + ); + } + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + url: data.url, + typebot: data.typebot, + id: { + not: botId, + }, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Typebot already exists'); + } + + if (data.triggerType === 'keyword') { + if (!data.triggerOperator || !data.triggerValue) { + throw new Error('Trigger operator and value are required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + 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, + url: data.url, + typebot: data.typebot, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + }, + }); + + return bot; + } catch (error) { + this.logger.error(error); + throw new Error('Error updating typebot'); + } } - public async startTypebot(instance: InstanceDto, data: any) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async deleteBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.startTypebot(instance, data); + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const typebot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!typebot) { + throw new Error('Typebot not found'); + } + + if (typebot.instanceId !== instanceId) { + throw new Error('Typebot not found'); + } + try { + await this.prismaRepository.integrationSession.deleteMany({ + where: { + botId: botId, + }, + }); + + await this.botRepository.delete({ + where: { + id: botId, + }, + }); + + return { typebot: { id: botId } }; + } catch (error) { + this.logger.error(error); + throw new Error('Error deleting typebot'); + } } + // Settings public async settings(instance: InstanceDto, data: any) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.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, + typebotIdFallback: data.typebotIdFallback, + ignoreJids: data.ignoreJids, + }, + }); + + return { + expire: updateSettings.expire, + keywordFinish: updateSettings.keywordFinish, + delayMessage: updateSettings.delayMessage, + unknownMessage: updateSettings.unknownMessage, + listeningFromMe: updateSettings.listeningFromMe, + stopBotFromMe: updateSettings.stopBotFromMe, + keepOpen: updateSettings.keepOpen, + debounceTime: updateSettings.debounceTime, + typebotIdFallback: updateSettings.typebotIdFallback, + ignoreJids: updateSettings.ignoreJids, + }; + } + + const newSetttings = await this.settingsRepository.create({ + data: { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + typebotIdFallback: data.typebotIdFallback, + ignoreJids: data.ignoreJids, + instanceId: instanceId, + }, + }); + + return { + expire: newSetttings.expire, + keywordFinish: newSetttings.keywordFinish, + delayMessage: newSetttings.delayMessage, + unknownMessage: newSetttings.unknownMessage, + listeningFromMe: newSetttings.listeningFromMe, + stopBotFromMe: newSetttings.stopBotFromMe, + keepOpen: newSetttings.keepOpen, + debounceTime: newSetttings.debounceTime, + typebotIdFallback: newSetttings.typebotIdFallback, + ignoreJids: newSetttings.ignoreJids, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } } public async fetchSettings(instance: InstanceDto) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.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: [], + typebotIdFallback: null, + fallback: null, + }; + } + + return { + expire: settings.expire, + keywordFinish: settings.keywordFinish, + delayMessage: settings.delayMessage, + unknownMessage: settings.unknownMessage, + listeningFromMe: settings.listeningFromMe, + stopBotFromMe: settings.stopBotFromMe, + keepOpen: settings.keepOpen, + ignoreJids: settings.ignoreJids, + typebotIdFallback: settings.typebotIdFallback, + fallback: settings.Fallback, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching default settings'); + } + } + + // Sessions + public async startBot(instance: InstanceDto, data: any) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); + + if (data.remoteJid === 'status@broadcast') return; + + const instanceData = await this.prismaRepository.instance.findFirst({ + where: { + name: instance.instanceName, + }, + }); + + if (!instanceData) throw new Error('Instance not found'); + + const remoteJid = data.remoteJid; + const url = data.url; + const typebot = data.typebot; + const startSession = data.startSession; + const variables = data.variables; + let expire = data?.typebot?.expire; + let keywordFinish = data?.typebot?.keywordFinish; + let delayMessage = data?.typebot?.delayMessage; + let unknownMessage = data?.typebot?.unknownMessage; + let listeningFromMe = data?.typebot?.listeningFromMe; + let stopBotFromMe = data?.typebot?.stopBotFromMe; + let keepOpen = data?.typebot?.keepOpen; + + const defaultSettingCheck = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceData.id, + }, + }); + + if (defaultSettingCheck?.ignoreJids) { + const ignoreJids: any = defaultSettingCheck.ignoreJids; + + let ignoreGroups = false; + let ignoreContacts = false; + + if (ignoreJids.includes('@g.us')) { + ignoreGroups = true; + } + + if (ignoreJids.includes('@s.whatsapp.net')) { + ignoreContacts = true; + } + + if (ignoreGroups && remoteJid.includes('@g.us')) { + this.logger.warn('Ignoring message from group: ' + remoteJid); + throw new Error('Group not allowed'); + } + + if (ignoreContacts && remoteJid.includes('@s.whatsapp.net')) { + this.logger.warn('Ignoring message from contact: ' + remoteJid); + throw new Error('Contact not allowed'); + } + + if (ignoreJids.includes(remoteJid)) { + this.logger.warn('Ignoring message from jid: ' + remoteJid); + throw new Error('Jid not allowed'); + } + } + + if ( + !expire || + !keywordFinish || + !delayMessage || + !unknownMessage || + !listeningFromMe || + !stopBotFromMe || + !keepOpen + ) { + if (!expire) expire = defaultSettingCheck?.expire || 0; + if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; + if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000; + if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; + if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false; + if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; + if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false; + + if (!defaultSettingCheck) { + await this.settings(instance, { + expire: expire, + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + }); + } + } + + const prefilledVariables = { + remoteJid: remoteJid, + instanceName: instance.instanceName, + serverUrl: configService.get('SERVER').URL, + apiKey: configService.get('AUTHENTICATION').API_KEY.KEY, + ownerJid: instanceData.number, + }; + + if (variables?.length) { + variables.forEach((variable: { name: string | number; value: string }) => { + prefilledVariables[variable.name] = variable.value; + }); + } + + if (startSession) { + let findBot: any = await this.botRepository.findFirst({ + where: { + url: url, + typebot: typebot, + instanceId: instanceData.id, + }, + }); + + if (!findBot) { + findBot = await this.botRepository.create({ + data: { + enabled: true, + url: url, + typebot: typebot, + expire: expire, + triggerType: 'none', + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + instanceId: instanceData.id, + }, + }); + } + + await this.prismaRepository.integrationSession.deleteMany({ + where: { + remoteJid: remoteJid, + instanceId: instanceData.id, + botId: { not: null }, + }, + }); + + const response = await this.typebotService.createNewSession(instanceData, { + enabled: true, + url: url, + typebot: typebot, + remoteJid: remoteJid, + expire: expire, + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + prefilledVariables: prefilledVariables, + typebotId: findBot.id, + }); + + if (response.sessionId) { + await this.typebotService.sendWAMessage( + instanceData, + response.session, + { + expire: expire, + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + }, + remoteJid, + response.messages, + response.input, + response.clientSideActions, + ); + + this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { + remoteJid: remoteJid, + url: url, + typebot: typebot, + prefilledVariables: prefilledVariables, + sessionId: `${response.sessionId}`, + }); + } else { + throw new Error('Session ID not found in response'); + } + } else { + const id = Math.floor(Math.random() * 10000000000).toString(); + + try { + const version = configService.get('TYPEBOT').API_VERSION; + let url: string; + let reqData: {}; + if (version === 'latest') { + url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; + + reqData = { + prefilledVariables: prefilledVariables, + }; + } else { + url = `${data.url}/api/v1/sendMessage`; + + reqData = { + startParams: { + publicId: data.typebot, + prefilledVariables: prefilledVariables, + }, + }; + } + const request = await axios.post(url, reqData); + + await this.typebotService.sendWAMessage( + instanceData, + null, + { + expire: expire, + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + }, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); + + this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { + remoteJid: remoteJid, + url: url, + typebot: typebot, + variables: variables, + sessionId: id, + }); + } catch (error) { + this.logger.error(error); + return; + } + } + + return { + typebot: { + ...instance, + typebot: { + url: url, + remoteJid: remoteJid, + typebot: typebot, + prefilledVariables: prefilledVariables, + }, + }, + }; } public async changeStatus(instance: InstanceDto, data: any) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.changeStatus(instance, data); + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const remoteJid = data.remoteJid; + const status = data.status; + + const defaultSettingCheck = await this.settingsRepository.findFirst({ + where: { + instanceId, + }, + }); + + if (status === 'delete') { + await this.sessionRepository.deleteMany({ + where: { + remoteJid: remoteJid, + instanceId: instanceId, + botId: { not: null }, + }, + }); + + return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; + } + + if (status === 'closed') { + if (defaultSettingCheck?.keepOpen) { + await this.sessionRepository.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + botId: { not: null }, + }, + data: { + status: status, + }, + }); + } else { + await this.sessionRepository.deleteMany({ + where: { + remoteJid: remoteJid, + instanceId: instanceId, + botId: { not: null }, + }, + }); + } + + return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; + } + + const session = await this.sessionRepository.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + botId: { not: null }, + }, + data: { + status: status, + }, + }); + + const typebotData = { + remoteJid: remoteJid, + status: status, + session, + }; + + this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData); + + return { typebot: { ...instance, typebot: typebotData } }; + } catch (error) { + this.logger.error(error); + throw new Error('Error changing status'); + } } - public async fetchSessions(instance: InstanceDto, typebotId: string) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.fetchSessions(instance, typebotId); + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const typebot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (typebot && typebot.instanceId !== instanceId) { + throw new Error('Typebot not found'); + } + + return await this.sessionRepository.findMany({ + where: { + instanceId: instanceId, + remoteJid, + botId: botId ?? { not: null }, + }, + }); + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching sessions'); + } } - public async ignoreJid(instance: InstanceDto, data: TypebotIgnoreJidDto) { - if (!configService.get('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled'); + public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { + if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - return this.typebotService.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({ @@ -83,8 +978,143 @@ export class TypebotController { msg: any; pushName?: string; }) { - if (!configService.get('TYPEBOT').ENABLED) return; + if (!this.integrationEnabled) return; - await this.typebotService.sendTypebot(instance, remoteJid, msg); + try { + const instanceData = await this.prismaRepository.instance.findFirst({ + where: { + name: instance.instanceName, + }, + }); + + if (!instanceData) throw new Error('Instance not found'); + + const settings = await this.prismaRepository.typebotSetting.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; + + const url = findBot?.url; + const typebot = findBot?.typebot; + let expire = findBot?.expire; + let keywordFinish = findBot?.keywordFinish; + let delayMessage = findBot?.delayMessage; + let unknownMessage = findBot?.unknownMessage; + let listeningFromMe = findBot?.listeningFromMe; + let stopBotFromMe = findBot?.stopBotFromMe; + let keepOpen = findBot?.keepOpen; + let debounceTime = findBot?.debounceTime; + + if ( + !expire || + !keywordFinish || + !delayMessage || + !unknownMessage || + !listeningFromMe || + !stopBotFromMe || + !keepOpen + ) { + 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.sessionRepository.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.typebotService.processTypebot( + instanceData, + remoteJid, + msg, + session, + findBot, + url, + expire, + typebot, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, + debouncedContent, + ); + }); + } else { + await this.typebotService.processTypebot( + instanceData, + remoteJid, + msg, + session, + findBot, + url, + expire, + typebot, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, + content, + ); + } + + if (session && !session.awaitUser) return; + } catch (error) { + this.logger.error(error); + return; + } } } diff --git a/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts b/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts index eedb8bc5..a7565236 100644 --- a/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts +++ b/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts @@ -1,14 +1,5 @@ import { TriggerOperator, TriggerType } from '@prisma/client'; -export class Session { - remoteJid?: string; - sessionId?: string; - status?: string; - createdAt?: number; - updateAt?: number; - prefilledVariables?: PrefilledVariables; -} - export class PrefilledVariables { remoteJid?: string; pushName?: string; @@ -47,8 +38,3 @@ export class TypebotSettingDto { typebotIdFallback?: string; ignoreJids?: any; } - -export class TypebotIgnoreJidDto { - remoteJid?: string; - action?: string; -} diff --git a/src/api/integrations/chatbot/typebot/routes/typebot.router.ts b/src/api/integrations/chatbot/typebot/routes/typebot.router.ts index 990cb88c..f556f94f 100644 --- a/src/api/integrations/chatbot/typebot/routes/typebot.router.ts +++ b/src/api/integrations/chatbot/typebot/routes/typebot.router.ts @@ -1,8 +1,9 @@ import { RouterBroker } from '@api/abstract/abstract.router'; +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; -import { TypebotDto, TypebotIgnoreJidDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; -import { typebotController } from '@api/server.module'; +import { TypebotDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { HttpStatus } from '@api/routes/index.router'; +import { typebotController } from '@api/server.module'; import { instanceSchema, typebotIgnoreJidSchema, @@ -22,7 +23,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: typebotSchema, ClassRef: TypebotDto, - execute: (instance, data) => typebotController.createTypebot(instance, data), + execute: (instance, data) => typebotController.createBot(instance, data), }); res.status(HttpStatus.CREATED).json(response); @@ -32,7 +33,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => typebotController.findTypebot(instance), + execute: (instance) => typebotController.findBot(instance), }); res.status(HttpStatus.OK).json(response); @@ -42,7 +43,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => typebotController.fetchTypebot(instance, req.params.typebotId), + execute: (instance) => typebotController.fetchBot(instance, req.params.typebotId), }); res.status(HttpStatus.OK).json(response); @@ -52,7 +53,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: typebotSchema, 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); @@ -62,7 +63,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: instanceSchema, ClassRef: InstanceDto, - execute: (instance) => typebotController.deleteTypebot(instance, req.params.typebotId), + execute: (instance) => typebotController.deleteBot(instance, req.params.typebotId), }); res.status(HttpStatus.OK).json(response); @@ -92,7 +93,7 @@ export class TypebotRouter extends RouterBroker { request: req, schema: typebotStartSchema, ClassRef: InstanceDto, - execute: (instance, data) => typebotController.startTypebot(instance, data), + execute: (instance, data) => typebotController.startBot(instance, data), }); res.status(HttpStatus.OK).json(response); @@ -118,10 +119,10 @@ export class TypebotRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { - const response = await this.dataValidate({ + const response = await this.dataValidate({ request: req, schema: typebotIgnoreJidSchema, - ClassRef: TypebotIgnoreJidDto, + ClassRef: IgnoreJidDto, execute: (instance, data) => typebotController.ignoreJid(instance, data), }); diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index a1f6ca50..e425b5d0 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -1,12 +1,9 @@ -import { InstanceDto } from '@api/dto/instance.dto'; -import { TypebotDto, TypebotIgnoreJidDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { Events } from '@api/types/wa.types'; -import { Auth, ConfigService, HttpServer, S3, Typebot } from '@config/env.config'; +import { Auth, ConfigService, HttpServer, Typebot } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { Instance, IntegrationSession, Message, Typebot as TypebotModel } from '@prisma/client'; -import { advancedOperatorsSearch } from '@utils/advancedOperatorsSearch'; +import { getConversationMessage } from '@utils/getConversationMessage'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; @@ -17,980 +14,8 @@ export class TypebotService { private readonly prismaRepository: PrismaRepository, ) {} - private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - private readonly logger = new Logger('TypebotService'); - public async create(instance: InstanceDto, data: TypebotDto) { - 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.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; - if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; - if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; - if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; - if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; - if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; - if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; - if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; - if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; - - if (!defaultSettingCheck) { - await this.setDefaultSettings(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.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - instanceId: instanceId, - }, - }); - - if (checkTriggerAll && data.triggerType === 'all') { - throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active'); - } - - const checkDuplicate = await this.prismaRepository.typebot.findFirst({ - where: { - url: data.url, - typebot: data.typebot, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Typebot already exists'); - } - - if (data.triggerType === 'keyword') { - if (!data.triggerOperator || !data.triggerValue) { - throw new Error('Trigger operator and value are required'); - } - - const checkDuplicate = await this.prismaRepository.typebot.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.prismaRepository.typebot.findFirst({ - where: { - triggerValue: data.triggerValue, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const typebot = await this.prismaRepository.typebot.create({ - data: { - enabled: data.enabled, - description: data.description, - url: data.url, - typebot: data.typebot, - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - instanceId: instanceId, - triggerType: data.triggerType, - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - ignoreJids: data.ignoreJids, - }, - }); - - return typebot; - } catch (error) { - this.logger.error(error); - throw new Error('Error creating typebot'); - } - } - - public async fetch(instance: InstanceDto, typebotId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.prismaRepository.typebot.findFirst({ - where: { - id: typebotId, - }, - include: { - sessions: true, - }, - }); - - if (!typebot) { - throw new Error('Typebot not found'); - } - - if (typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - - return typebot; - } - - public async update(instance: InstanceDto, typebotId: string, data: any) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.prismaRepository.typebot.findFirst({ - where: { - id: typebotId, - }, - }); - - if (!typebot) { - throw new Error('Typebot not found'); - } - - if (typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - - if (data.triggerType === 'all') { - const checkTriggerAll = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - id: { - not: typebotId, - }, - instanceId: instanceId, - }, - }); - - if (checkTriggerAll) { - throw new Error( - 'You already have a typebot with an "All" trigger, you cannot have more bots while it is active', - ); - } - } - - const checkDuplicate = await this.prismaRepository.typebot.findFirst({ - where: { - url: data.url, - typebot: data.typebot, - id: { - not: typebotId, - }, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Typebot already exists'); - } - - if (data.triggerType === 'keyword') { - if (!data.triggerOperator || !data.triggerValue) { - throw new Error('Trigger operator and value are required'); - } - - const checkDuplicate = await this.prismaRepository.typebot.findFirst({ - where: { - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - id: { - not: typebotId, - }, - 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.prismaRepository.typebot.findFirst({ - where: { - triggerValue: data.triggerValue, - id: { not: typebotId }, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Trigger already exists'); - } - } - - try { - const typebot = await this.prismaRepository.typebot.update({ - where: { - id: typebotId, - }, - data: { - enabled: data.enabled, - url: data.url, - typebot: data.typebot, - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - triggerType: data.triggerType, - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - ignoreJids: data.ignoreJids, - }, - }); - - return typebot; - } catch (error) { - this.logger.error(error); - throw new Error('Error updating typebot'); - } - } - - public async find(instance: InstanceDto): Promise { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebots = await this.prismaRepository.typebot.findMany({ - where: { - instanceId: instanceId, - }, - include: { - sessions: true, - }, - }); - - if (!typebots.length) { - return null; - } - - return typebots; - } - - public async delete(instance: InstanceDto, typebotId: string) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.prismaRepository.typebot.findFirst({ - where: { - id: typebotId, - }, - }); - - if (!typebot) { - throw new Error('Typebot not found'); - } - - if (typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - try { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - typebotId: typebotId, - }, - }); - - await this.prismaRepository.typebot.delete({ - where: { - id: typebotId, - }, - }); - - return { typebot: { id: typebotId } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error deleting typebot'); - } - } - - public async setDefaultSettings(instance: InstanceDto, data: any) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (settings) { - const updateSettings = await this.prismaRepository.typebotSetting.update({ - where: { - id: settings.id, - }, - data: { - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - typebotIdFallback: data.typebotIdFallback, - ignoreJids: data.ignoreJids, - }, - }); - - return { - expire: updateSettings.expire, - keywordFinish: updateSettings.keywordFinish, - delayMessage: updateSettings.delayMessage, - unknownMessage: updateSettings.unknownMessage, - listeningFromMe: updateSettings.listeningFromMe, - stopBotFromMe: updateSettings.stopBotFromMe, - keepOpen: updateSettings.keepOpen, - debounceTime: updateSettings.debounceTime, - typebotIdFallback: updateSettings.typebotIdFallback, - ignoreJids: updateSettings.ignoreJids, - }; - } - - const newSetttings = await this.prismaRepository.typebotSetting.create({ - data: { - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - typebotIdFallback: data.typebotIdFallback, - ignoreJids: data.ignoreJids, - instanceId: instanceId, - }, - }); - - return { - expire: newSetttings.expire, - keywordFinish: newSetttings.keywordFinish, - delayMessage: newSetttings.delayMessage, - unknownMessage: newSetttings.unknownMessage, - listeningFromMe: newSetttings.listeningFromMe, - stopBotFromMe: newSetttings.stopBotFromMe, - keepOpen: newSetttings.keepOpen, - debounceTime: newSetttings.debounceTime, - typebotIdFallback: newSetttings.typebotIdFallback, - ignoreJids: newSetttings.ignoreJids, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error setting default settings'); - } - } - - public async fetchDefaultSettings(instance: InstanceDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instanceId, - }, - include: { - Fallback: true, - }, - }); - - if (!settings) { - return { - expire: 0, - keywordFinish: '', - delayMessage: 0, - unknownMessage: '', - listeningFromMe: false, - stopBotFromMe: false, - keepOpen: false, - ignoreJids: [], - typebotIdFallback: null, - fallback: null, - }; - } - - return { - expire: settings.expire, - keywordFinish: settings.keywordFinish, - delayMessage: settings.delayMessage, - unknownMessage: settings.unknownMessage, - listeningFromMe: settings.listeningFromMe, - stopBotFromMe: settings.stopBotFromMe, - keepOpen: settings.keepOpen, - ignoreJids: settings.ignoreJids, - typebotIdFallback: settings.typebotIdFallback, - fallback: settings.Fallback, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching default settings'); - } - } - - public async ignoreJid(instance: InstanceDto, data: TypebotIgnoreJidDto) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const settings = await this.prismaRepository.typebotSetting.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.prismaRepository.typebotSetting.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 fetchSessions(instance: InstanceDto, typebotId?: string, remoteJid?: string) { - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.prismaRepository.typebot.findFirst({ - where: { - id: typebotId, - }, - }); - - if (typebot && typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - - return await this.prismaRepository.integrationSession.findMany({ - where: { - instanceId: instanceId, - remoteJid, - typebotId: typebotId ?? { not: null }, - }, - include: { - Typebot: true, - }, - }); - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching 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 remoteJid = data.remoteJid; - const status = data.status; - - const defaultSettingCheck = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId, - }, - }); - - if (status === 'delete') { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - instanceId: instanceId, - typebotId: { not: null }, - }, - }); - - return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; - } - - if (status === 'closed') { - if (defaultSettingCheck?.keepOpen) { - await this.prismaRepository.integrationSession.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - typebotId: { not: null }, - }, - data: { - status: status, - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - instanceId: instanceId, - typebotId: { not: null }, - }, - }); - } - - return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; - } - - const session = await this.prismaRepository.integrationSession.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - typebotId: { not: null }, - }, - data: { - status: status, - }, - }); - - const typebotData = { - remoteJid: remoteJid, - status: status, - session, - }; - - this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData); - - return { typebot: { ...instance, typebot: typebotData } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error changing status'); - } - } - - public async startTypebot(instance: InstanceDto, data: any) { - if (data.remoteJid === 'status@broadcast') return; - - const instanceData = await this.prismaRepository.instance.findFirst({ - where: { - name: instance.instanceName, - }, - }); - - if (!instanceData) throw new Error('Instance not found'); - - const remoteJid = data.remoteJid; - const url = data.url; - const typebot = data.typebot; - const startSession = data.startSession; - const variables = data.variables; - let expire = data?.typebot?.expire; - let keywordFinish = data?.typebot?.keywordFinish; - let delayMessage = data?.typebot?.delayMessage; - let unknownMessage = data?.typebot?.unknownMessage; - let listeningFromMe = data?.typebot?.listeningFromMe; - let stopBotFromMe = data?.typebot?.stopBotFromMe; - let keepOpen = data?.typebot?.keepOpen; - - const defaultSettingCheck = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instanceData.id, - }, - }); - - if (defaultSettingCheck?.ignoreJids) { - const ignoreJids: any = defaultSettingCheck.ignoreJids; - - let ignoreGroups = false; - let ignoreContacts = false; - - if (ignoreJids.includes('@g.us')) { - ignoreGroups = true; - } - - if (ignoreJids.includes('@s.whatsapp.net')) { - ignoreContacts = true; - } - - if (ignoreGroups && remoteJid.includes('@g.us')) { - this.logger.warn('Ignoring message from group: ' + remoteJid); - throw new Error('Group not allowed'); - } - - if (ignoreContacts && remoteJid.includes('@s.whatsapp.net')) { - this.logger.warn('Ignoring message from contact: ' + remoteJid); - throw new Error('Contact not allowed'); - } - - if (ignoreJids.includes(remoteJid)) { - this.logger.warn('Ignoring message from jid: ' + remoteJid); - throw new Error('Jid not allowed'); - } - } - - if ( - !expire || - !keywordFinish || - !delayMessage || - !unknownMessage || - !listeningFromMe || - !stopBotFromMe || - !keepOpen - ) { - if (!expire) expire = defaultSettingCheck?.expire || 0; - if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; - if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000; - if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; - if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false; - if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; - if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false; - - if (!defaultSettingCheck) { - await this.setDefaultSettings(instance, { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }); - } - } - - const prefilledVariables = { - remoteJid: remoteJid, - instanceName: instance.instanceName, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, - ownerJid: instanceData.number, - }; - - if (variables?.length) { - variables.forEach((variable: { name: string | number; value: string }) => { - prefilledVariables[variable.name] = variable.value; - }); - } - - if (startSession) { - let findTypebot: any = await this.prismaRepository.typebot.findFirst({ - where: { - url: url, - typebot: typebot, - instanceId: instanceData.id, - }, - }); - - if (!findTypebot) { - findTypebot = await this.prismaRepository.typebot.create({ - data: { - enabled: true, - url: url, - typebot: typebot, - expire: expire, - triggerType: 'none', - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - instanceId: instanceData.id, - }, - }); - } - - await this.prismaRepository.integrationSession.deleteMany({ - where: { - remoteJid: remoteJid, - instanceId: instanceData.id, - typebotId: { not: null }, - }, - }); - - const response = await this.createNewSession(instanceData, { - enabled: true, - url: url, - typebot: typebot, - remoteJid: remoteJid, - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - prefilledVariables: prefilledVariables, - typebotId: findTypebot.id, - }); - - if (response.sessionId) { - await this.sendWAMessage( - instanceData, - response.session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - response.messages, - response.input, - response.clientSideActions, - ); - - this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { - remoteJid: remoteJid, - url: url, - typebot: typebot, - prefilledVariables: prefilledVariables, - sessionId: `${response.sessionId}`, - }); - } else { - throw new Error('Session ID not found in response'); - } - } else { - const id = Math.floor(Math.random() * 10000000000).toString(); - - try { - const version = this.configService.get('TYPEBOT').API_VERSION; - let url: string; - let reqData: {}; - if (version === 'latest') { - url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; - - reqData = { - prefilledVariables: prefilledVariables, - }; - } else { - url = `${data.url}/api/v1/sendMessage`; - - reqData = { - startParams: { - publicId: data.typebot, - prefilledVariables: prefilledVariables, - }, - }; - } - const request = await axios.post(url, reqData); - - await this.sendWAMessage( - instanceData, - null, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); - - this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { - remoteJid: remoteJid, - url: url, - typebot: typebot, - variables: variables, - sessionId: id, - }); - } catch (error) { - this.logger.error(error); - return; - } - } - - return { - typebot: { - ...instance, - typebot: { - url: url, - remoteJid: remoteJid, - typebot: typebot, - prefilledVariables: prefilledVariables, - }, - }, - }; - } - - private getTypeMessage(msg: any) { - let mediaId: string; - - if (this.configService.get('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}` : undefined, - videoMessage: msg?.message?.videoMessage ? `videoMessage|${mediaId}` : undefined, - documentMessage: msg?.message?.documentMessage ? `documentMessage|${mediaId}` : undefined, - documentWithCaptionMessage: msg?.message?.auddocumentWithCaptionMessageioMessage - ? `documentWithCaptionMessage|${mediaId}` - : undefined, - }; - - const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown'; - - return { ...types, messageType }; - } - - private getMessageContent(types: any) { - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); - - const result = typeKey ? types[typeKey] : undefined; - - return result; - } - - private getConversationMessage(msg: any) { - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - return messageContent; - } - public async createNewSession(instance: Instance, data: any) { if (data.remoteJid === 'status@broadcast') return; const id = Math.floor(Math.random() * 10000000000).toString(); @@ -1051,7 +76,7 @@ export class TypebotService { ownerJid: instance.number, }, awaitUser: false, - typebotId: data.typebotId, + botId: data.botId, instanceId: instance.id, }, }); @@ -1311,359 +336,7 @@ export class TypebotService { } } - public async findTypebotByTrigger(content: string, instanceId: string) { - // Check for triggerType 'all' - const findTriggerAll = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'all', - instanceId: instanceId, - }, - }); - - if (findTriggerAll) return findTriggerAll; - - const findTriggerAdvanced = await this.prismaRepository.typebot.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 this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'equals', - triggerValue: content, - instanceId: instanceId, - }, - }); - - if (findTriggerEquals) return findTriggerEquals; - - // Check for regex match - const findRegex = await this.prismaRepository.typebot.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 this.prismaRepository.typebot.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 this.prismaRepository.typebot.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 this.prismaRepository.typebot.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 this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (fallback?.typebotIdFallback) { - const findFallback = await this.prismaRepository.typebot.findFirst({ - where: { - id: fallback.typebotIdFallback, - }, - }); - - if (findFallback) return findFallback; - } - - return null; - } - - private processDebounce(content: string, remoteJid: string, debounceTime: number, callback: any) { - if (this.userMessageDebounce[remoteJid]) { - this.userMessageDebounce[remoteJid].message += ` ${content}`; - this.logger.log('message debounced: ' + this.userMessageDebounce[remoteJid].message); - clearTimeout(this.userMessageDebounce[remoteJid].timeoutId); - } else { - this.userMessageDebounce[remoteJid] = { - message: content, - timeoutId: null, - }; - } - - this.userMessageDebounce[remoteJid].timeoutId = setTimeout(() => { - const myQuestion = this.userMessageDebounce[remoteJid].message; - this.logger.log('Debounce complete. Processing message: ' + myQuestion); - - delete this.userMessageDebounce[remoteJid]; - callback(myQuestion); - }, debounceTime * 1000); - } - - public async sendTypebot(instance: InstanceDto, remoteJid: string, msg: Message) { - try { - const instanceData = await this.prismaRepository.instance.findFirst({ - where: { - name: instance.instanceName, - }, - }); - - if (!instanceData) throw new Error('Instance not found'); - - const settings = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (settings?.ignoreJids) { - const ignoreJids: any = settings.ignoreJids; - - 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; - } - - if (ignoreContacts && remoteJid.endsWith('@s.whatsapp.net')) { - this.logger.warn('Ignoring message from contact: ' + remoteJid); - return; - } - - if (ignoreJids.includes(remoteJid)) { - this.logger.warn('Ignoring message from jid: ' + remoteJid); - return; - } - } - - let session = await this.prismaRepository.integrationSession.findFirst({ - where: { - remoteJid: remoteJid, - instanceId: instance.instanceId, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (session) { - if (session.status !== 'closed' && !session.typebotId) { - this.logger.warn('Session is already opened in another integration'); - return; - } else if (!session.typebotId) { - session = null; - } - } - - const content = this.getConversationMessage(msg); - - let findTypebot = null; - - if (!session) { - findTypebot = await this.findTypebotByTrigger(content, instance.instanceId); - - if (!findTypebot) { - return; - } - } else { - findTypebot = await this.prismaRepository.typebot.findFirst({ - where: { - id: session.typebotId, - }, - }); - } - - const url = findTypebot?.url; - const typebot = findTypebot?.typebot; - let expire = findTypebot?.expire; - let keywordFinish = findTypebot?.keywordFinish; - let delayMessage = findTypebot?.delayMessage; - let unknownMessage = findTypebot?.unknownMessage; - let listeningFromMe = findTypebot?.listeningFromMe; - let stopBotFromMe = findTypebot?.stopBotFromMe; - let keepOpen = findTypebot?.keepOpen; - let debounceTime = findTypebot?.debounceTime; - - if ( - !expire || - !keywordFinish || - !delayMessage || - !unknownMessage || - !listeningFromMe || - !stopBotFromMe || - !keepOpen - ) { - 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(content, remoteJid, debounceTime, async (debouncedContent) => { - await this.processTypebot( - instanceData, - remoteJid, - msg, - session, - findTypebot, - url, - expire, - typebot, - keywordFinish, - delayMessage, - unknownMessage, - listeningFromMe, - stopBotFromMe, - keepOpen, - debouncedContent, - ); - }); - } else { - await this.processTypebot( - instanceData, - remoteJid, - msg, - session, - findTypebot, - url, - expire, - typebot, - keywordFinish, - delayMessage, - unknownMessage, - listeningFromMe, - stopBotFromMe, - keepOpen, - content, - ); - } - - if (session && !session.awaitUser) return; - } catch (error) { - this.logger.error(error); - return; - } - } - - private async processTypebot( + public async processTypebot( instance: Instance, remoteJid: string, msg: Message, @@ -1702,7 +375,7 @@ export class TypebotService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - typebotId: findTypebot.id, + botId: findTypebot.id, remoteJid: remoteJid, }, }); @@ -1719,7 +392,7 @@ export class TypebotService { listeningFromMe: listeningFromMe, remoteJid: remoteJid, pushName: msg.pushName, - typebotId: findTypebot.id, + botId: findTypebot.id, }); if (data.session) { @@ -1745,7 +418,7 @@ export class TypebotService { ); if (data.messages.length === 0) { - const content = this.getConversationMessage(msg.message); + const content = getConversationMessage(msg.message); if (!content) { if (unknownMessage) { @@ -1776,7 +449,7 @@ export class TypebotService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - typebotId: findTypebot.id, + botId: findTypebot.id, remoteJid: remoteJid, }, }); @@ -1901,7 +574,7 @@ export class TypebotService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - typebotId: findTypebot.id, + botId: findTypebot.id, remoteJid: remoteJid, }, }); @@ -1993,7 +666,7 @@ export class TypebotService { } else { await this.prismaRepository.integrationSession.deleteMany({ where: { - typebotId: findTypebot.id, + botId: findTypebot.id, remoteJid: remoteJid, }, }); diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 2f18075c..6dc1fa17 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -108,12 +108,12 @@ export const webhookController = new WebhookController(prismaRepository, waMonit // chatbots 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); -export const openaiController = new OpenaiController(openaiService); +export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor); const difyService = new DifyService(waMonitor, configService, prismaRepository); -export const difyController = new DifyController(difyService); +export const difyController = new DifyController(difyService, prismaRepository, waMonitor); logger.info('Module - ON'); From 9209c9992da7526c495f7b16c69fd10f42046d9c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 Aug 2024 13:47:41 -0300 Subject: [PATCH 3/4] refactor: event integration --- .../integrations/event/event.controller.ts | 19 ++++++++++++ .../controllers/rabbitmq.controller.ts | 29 +++++++------------ .../event/sqs/controllers/sqs.controller.ts | 28 +++++++----------- .../webhook/controllers/webhook.controller.ts | 17 +++-------- .../controllers/websocket.controller.ts | 28 +++++++----------- 5 files changed, 54 insertions(+), 67 deletions(-) diff --git a/src/api/integrations/event/event.controller.ts b/src/api/integrations/event/event.controller.ts index d1281fda..e0a85946 100644 --- a/src/api/integrations/event/event.controller.ts +++ b/src/api/integrations/event/event.controller.ts @@ -3,6 +3,25 @@ import { rabbitmqController, sqsController, webhookController, websocketControll import { WAMonitoringService } from '@api/services/monitor.service'; 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; + get(instanceName: string): Promise; + emit({ instanceName, origin, event, data, serverUrl, dateTime, sender, apiKey, local }: EmitData): Promise; +} + export class EventController { public prismaRepository: PrismaRepository; public waMonitor: WAMonitoringService; diff --git a/src/api/integrations/event/rabbitmq/controllers/rabbitmq.controller.ts b/src/api/integrations/event/rabbitmq/controllers/rabbitmq.controller.ts index af33ae6f..26c48b72 100644 --- a/src/api/integrations/event/rabbitmq/controllers/rabbitmq.controller.ts +++ b/src/api/integrations/event/rabbitmq/controllers/rabbitmq.controller.ts @@ -7,19 +7,19 @@ import { Logger } from '@config/logger.config'; import { NotFoundException } from '@exceptions'; 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; private readonly logger = new Logger(RabbitmqController.name); + integrationEnabled = configService.get('RABBITMQ')?.ENABLED; + constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { super(prismaRepository, waMonitor); } public async init(): Promise { - if (!configService.get('RABBITMQ')?.ENABLED) { - return; - } + if (!this.integrationEnabled) return; await new Promise((resolve, reject) => { const uri = configService.get('RABBITMQ').URI; @@ -62,6 +62,8 @@ export class RabbitmqController extends EventController { } public async set(instanceName: string, data: RabbitmqDto): Promise { + if (!this.integrationEnabled) return; + if (!data.enabled) { data.events = []; } else { @@ -91,6 +93,8 @@ export class RabbitmqController extends EventController { } public async get(instanceName: string): Promise { + if (!this.integrationEnabled) return; + if (undefined === this.monitor.waInstances[instanceName]) { throw new NotFoundException('Instance not found'); } @@ -117,19 +121,8 @@ export class RabbitmqController extends EventController { dateTime, sender, apiKey, - }: { - instanceName: string; - origin: string; - event: string; - data: Object; - serverUrl: string; - dateTime: string; - sender: string; - apiKey?: string; - }): Promise { - if (!configService.get('RABBITMQ')?.ENABLED) { - return; - } + }: EmitData): Promise { + if (!this.integrationEnabled) return; const instanceRabbitmq = await this.get(instanceName); const rabbitmqLocal = instanceRabbitmq?.events; diff --git a/src/api/integrations/event/sqs/controllers/sqs.controller.ts b/src/api/integrations/event/sqs/controllers/sqs.controller.ts index 519e2762..c1e9933d 100644 --- a/src/api/integrations/event/sqs/controllers/sqs.controller.ts +++ b/src/api/integrations/event/sqs/controllers/sqs.controller.ts @@ -7,20 +7,19 @@ import { configService, Log, Sqs } from '@config/env.config'; import { Logger } from '@config/logger.config'; 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 readonly logger = new Logger(SqsController.name); + integrationEnabled = configService.get('SQS')?.ENABLED; constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { super(prismaRepository, waMonitor); } public init(): void { - if (!configService.get('SQS')?.ENABLED) { - return; - } + if (!this.integrationEnabled) return; // eslint-disable-next-line @typescript-eslint/no-unused-vars new Promise((resolve, reject) => { @@ -48,6 +47,8 @@ export class SqsController extends EventController { } public async set(instanceName: string, data: SqsDto): Promise { + if (!this.integrationEnabled) return; + if (!data.enabled) { data.events = []; } else { @@ -77,6 +78,8 @@ export class SqsController extends EventController { } public async get(instanceName: string): Promise { + if (!this.integrationEnabled) return; + if (undefined === this.monitor.waInstances[instanceName]) { throw new NotFoundException('Instance not found'); } @@ -103,19 +106,8 @@ export class SqsController extends EventController { dateTime, sender, apiKey, - }: { - instanceName: string; - origin: string; - event: string; - data: Object; - serverUrl: string; - dateTime: string; - sender: string; - apiKey?: string; - }): Promise { - if (!configService.get('SQS')?.ENABLED) { - return; - } + }: EmitData): Promise { + if (!this.integrationEnabled) return; const instanceSqs = await this.get(instanceName); const sqsLocal = instanceSqs?.events; diff --git a/src/api/integrations/event/webhook/controllers/webhook.controller.ts b/src/api/integrations/event/webhook/controllers/webhook.controller.ts index fefe00b6..c1b84757 100644 --- a/src/api/integrations/event/webhook/controllers/webhook.controller.ts +++ b/src/api/integrations/event/webhook/controllers/webhook.controller.ts @@ -7,15 +7,16 @@ import { BadRequestException, NotFoundException } from '@exceptions'; import axios from 'axios'; import { isURL } from 'class-validator'; -import { EventController } from '../../event.controller'; +import { EmitData, EventController, EventControllerInterface } from '../../event.controller'; 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); constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { super(prismaRepository, waMonitor); } + integrationEnabled: boolean; public async set(instanceName: string, data: WebhookDto): Promise { if (!isURL(data.url, { require_tld: false })) { @@ -78,17 +79,7 @@ export class WebhookController extends EventController { sender, apiKey, local, - }: { - instanceName: string; - origin: string; - event: string; - data: Object; - serverUrl: string; - dateTime: string; - sender: string; - apiKey?: string; - local?: boolean; - }): Promise { + }: EmitData): Promise { const instanceWebhook = await this.get(instanceName); const webhookGlobal = configService.get('WEBHOOK'); const webhookLocal = instanceWebhook?.events; diff --git a/src/api/integrations/event/websocket/controllers/websocket.controller.ts b/src/api/integrations/event/websocket/controllers/websocket.controller.ts index 7bfaa1b4..1b28bba7 100644 --- a/src/api/integrations/event/websocket/controllers/websocket.controller.ts +++ b/src/api/integrations/event/websocket/controllers/websocket.controller.ts @@ -8,12 +8,13 @@ import { NotFoundException } from '@exceptions'; import { Server } from 'http'; 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 corsConfig: Array; private readonly logger = new Logger(WebsocketController.name); + integrationEnabled = configService.get('WEBSOCKET')?.ENABLED; constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { super(prismaRepository, waMonitor); @@ -21,9 +22,7 @@ export class WebsocketController extends EventController { } public init(httpServer: Server): void { - if (!configService.get('WEBSOCKET')?.ENABLED) { - return; - } + if (!this.integrationEnabled) return; this.socket = new SocketIO(httpServer, { cors: { @@ -59,6 +58,8 @@ export class WebsocketController extends EventController { } public async set(instanceName: string, data: WebsocketDto): Promise { + if (!this.integrationEnabled) return; + if (!data.enabled) { data.events = []; } else { @@ -88,6 +89,8 @@ export class WebsocketController extends EventController { } public async get(instanceName: string): Promise { + if (!this.integrationEnabled) return; + if (undefined === this.monitor.waInstances[instanceName]) { throw new NotFoundException('Instance not found'); } @@ -114,19 +117,8 @@ export class WebsocketController extends EventController { dateTime, sender, apiKey, - }: { - instanceName: string; - origin: string; - event: string; - data: Object; - serverUrl: string; - dateTime: string; - sender: string; - apiKey?: string; - }): Promise { - if (!configService.get('WEBSOCKET')?.ENABLED) { - return; - } + }: EmitData): Promise { + if (!this.integrationEnabled) return; const configEv = event.replace(/[.-]/gm, '_').toUpperCase(); const logEnabled = configService.get('LOG').LEVEL.includes('WEBSOCKET'); From e7d971867b4d417a2d4769b3f81cff62d5f40ff4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 Aug 2024 13:52:09 -0300 Subject: [PATCH 4/4] refactor: chatbot integration --- .../whatsapp/baileys/whatsapp.baileys.service.ts | 14 ++++++++------ .../whatsapp/business/whatsapp.business.service.ts | 13 +++++++------ src/api/integrations/chatbot/chatbot.controller.ts | 3 +++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts index f1c0378a..48be6750 100644 --- a/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/baileys/whatsapp.baileys.service.ts @@ -2010,12 +2010,14 @@ export class BaileysStartupService extends ChannelStartupService { ); } - 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').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, + isIntegration, + }); if (this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) await this.prismaRepository.message.create({ diff --git a/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts b/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts index f6b4b56e..127adb81 100644 --- a/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts +++ b/src/api/integrations/channel/whatsapp/business/whatsapp.business.service.ts @@ -923,12 +923,13 @@ export class BusinessStartupService extends ChannelStartupService { ); } - 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').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, diff --git a/src/api/integrations/chatbot/chatbot.controller.ts b/src/api/integrations/chatbot/chatbot.controller.ts index 7054f4f9..75aa4962 100644 --- a/src/api/integrations/chatbot/chatbot.controller.ts +++ b/src/api/integrations/chatbot/chatbot.controller.ts @@ -68,17 +68,20 @@ export class ChatbotController { remoteJid, msg, pushName, + isIntegration = false, }: { instance: InstanceDto; remoteJid: string; msg: any; pushName?: string; + isIntegration?: boolean; }): Promise { const emitData = { instance, remoteJid, msg, pushName, + isIntegration, }; // typebot await typebotController.emit(emitData);