diff --git a/.env.example b/.env.example index 1a320aa1..b2720d20 100644 --- a/.env.example +++ b/.env.example @@ -213,6 +213,12 @@ OPENAI_ENABLED=false # Dify - Environment variables DIFY_ENABLED=false +# n8n - Environment variables +N8N_ENABLED=false + +# EvoAI - Environment variables +EVOAI_ENABLED=false + # Cache - Environment variables # Redis Cache enabled CACHE_REDIS_ENABLED=true diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 74a232b2..9e964b97 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -693,3 +693,51 @@ model N8nSetting { Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String @unique } + +model Evoai { + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + description String? @db.VarChar(255) + agentUrl String? @db.VarChar(255) + apiKey String? @db.VarChar(255) + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer + ignoreJids Json? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer + triggerType TriggerType? + triggerOperator TriggerOperator? + triggerValue String? + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String + EvoaiSetting EvoaiSetting[] +} + +model EvoaiSetting { + id String @id @default(cuid()) + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer + ignoreJids Json? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id]) + evoaiIdFallback String? @db.VarChar(100) + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String @unique +} diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 4bd1b417..2814706b 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -108,6 +108,8 @@ model Instance { Pusher Pusher? N8n N8n[] N8nSetting N8nSetting[] + Evoai Evoai[] + EvoaiSetting EvoaiSetting? } model Session { @@ -694,3 +696,51 @@ model N8nSetting { Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String @unique } + +model Evoai { + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + description String? @db.VarChar(255) + agentUrl String? @db.VarChar(255) + apiKey String? @db.VarChar(255) + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer + ignoreJids Json? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer + triggerType TriggerType? + triggerOperator TriggerOperator? + triggerValue String? + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String + EvoaiSetting EvoaiSetting[] +} + +model EvoaiSetting { + id String @id @default(cuid()) + expire Int? @default(0) @db.Integer + keywordFinish String? @db.VarChar(100) + delayMessage Int? @db.Integer + unknownMessage String? @db.VarChar(100) + listeningFromMe Boolean? @default(false) @db.Boolean + stopBotFromMe Boolean? @default(false) @db.Boolean + keepOpen Boolean? @default(false) @db.Boolean + debounceTime Int? @db.Integer + ignoreJids Json? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id]) + evoaiIdFallback String? @db.VarChar(100) + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String @unique +} \ No newline at end of file diff --git a/src/api/integrations/chatbot/chatbot.controller.ts b/src/api/integrations/chatbot/chatbot.controller.ts index f09de5d0..4bd6fa87 100644 --- a/src/api/integrations/chatbot/chatbot.controller.ts +++ b/src/api/integrations/chatbot/chatbot.controller.ts @@ -2,6 +2,7 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { difyController, + evoaiController, evolutionBotController, flowiseController, n8nController, @@ -100,6 +101,8 @@ export class ChatbotController { await n8nController.emit(emitData); + await evoaiController.emit(emitData); + await flowiseController.emit(emitData); } diff --git a/src/api/integrations/chatbot/chatbot.router.ts b/src/api/integrations/chatbot/chatbot.router.ts index 32d2add0..10a52083 100644 --- a/src/api/integrations/chatbot/chatbot.router.ts +++ b/src/api/integrations/chatbot/chatbot.router.ts @@ -4,6 +4,7 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router'; import { Router } from 'express'; +import { EvoaiRouter } from './evoai/routes/evoai.router'; import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router'; import { FlowiseRouter } from './flowise/routes/flowise.router'; import { N8nRouter } from './n8n/routes/n8n.router'; @@ -21,5 +22,6 @@ export class ChatbotRouter { this.router.use('/dify', new DifyRouter(...guards).router); this.router.use('/flowise', new FlowiseRouter(...guards).router); this.router.use('/n8n', new N8nRouter(...guards).router); + this.router.use('/evoai', new EvoaiRouter(...guards).router); } } diff --git a/src/api/integrations/chatbot/chatbot.schema.ts b/src/api/integrations/chatbot/chatbot.schema.ts index f5117212..4712a70d 100644 --- a/src/api/integrations/chatbot/chatbot.schema.ts +++ b/src/api/integrations/chatbot/chatbot.schema.ts @@ -1,5 +1,6 @@ export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema'; export * from '@api/integrations/chatbot/dify/validate/dify.schema'; +export * from '@api/integrations/chatbot/evoai/validate/evoai.schema'; export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema'; export * from '@api/integrations/chatbot/flowise/validate/flowise.schema'; export * from '@api/integrations/chatbot/n8n/validate/n8n.schema'; diff --git a/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts b/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts new file mode 100644 index 00000000..2efb97ca --- /dev/null +++ b/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts @@ -0,0 +1,886 @@ +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; +import { InstanceDto } from '@api/dto/instance.dto'; +import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto'; +import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service'; +import { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; +import { configService, Evoai } from '@config/env.config'; +import { Logger } from '@config/logger.config'; +import { BadRequestException } from '@exceptions'; +import { Evoai as EvoaiModel } from '@prisma/client'; +import { getConversationMessage } from '@utils/getConversationMessage'; + +import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; + +export class EvoaiController extends ChatbotController implements ChatbotControllerInterface { + constructor( + private readonly evoaiService: EvoaiService, + prismaRepository: PrismaRepository, + waMonitor: WAMonitoringService, + ) { + super(prismaRepository, waMonitor); + + this.botRepository = this.prismaRepository.evoai; + this.settingsRepository = this.prismaRepository.evoaiSetting; + this.sessionRepository = this.prismaRepository.integrationSession; + } + + public readonly logger = new Logger('EvoaiController'); + + integrationEnabled = configService.get('EVOAI').ENABLED; + botRepository: any; + settingsRepository: any; + sessionRepository: any; + userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; + + // Bots + public async createBot(instance: InstanceDto, data: EvoaiDto) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if ( + !data.expire || + !data.keywordFinish || + !data.delayMessage || + !data.unknownMessage || + !data.listeningFromMe || + !data.stopBotFromMe || + !data.keepOpen || + !data.debounceTime || + !data.ignoreJids || + !data.splitMessages || + !data.timePerChar + ) { + const defaultSettingCheck = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire; + if (data.keywordFinish === undefined || data.keywordFinish === null) + data.keywordFinish = defaultSettingCheck.keywordFinish; + if (data.delayMessage === undefined || data.delayMessage === null) + data.delayMessage = defaultSettingCheck.delayMessage; + if (data.unknownMessage === undefined || data.unknownMessage === null) + data.unknownMessage = defaultSettingCheck.unknownMessage; + if (data.listeningFromMe === undefined || data.listeningFromMe === null) + data.listeningFromMe = defaultSettingCheck.listeningFromMe; + if (data.stopBotFromMe === undefined || data.stopBotFromMe === null) + data.stopBotFromMe = defaultSettingCheck.stopBotFromMe; + if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen; + if (data.debounceTime === undefined || data.debounceTime === null) + data.debounceTime = defaultSettingCheck.debounceTime; + if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids; + if (data.splitMessages === undefined || data.splitMessages === null) + data.splitMessages = defaultSettingCheck?.splitMessages ?? false; + if (data.timePerChar === undefined || data.timePerChar === null) + data.timePerChar = defaultSettingCheck?.timePerChar ?? 0; + + if (!defaultSettingCheck) { + await this.settings(instance, { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + ignoreJids: data.ignoreJids, + splitMessages: data.splitMessages, + timePerChar: data.timePerChar, + }); + } + } + + const checkTriggerAll = await this.botRepository.findFirst({ + where: { + enabled: true, + triggerType: 'all', + instanceId: instanceId, + }, + }); + + if (checkTriggerAll && data.triggerType === 'all') { + throw new Error('You already have a evoai with an "All" trigger, you cannot have more bots while it is active'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + instanceId: instanceId, + agentUrl: data.agentUrl, + apiKey: data.apiKey, + }, + }); + + if (checkDuplicate) { + throw new Error('Evoai 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, + agentUrl: data.agentUrl, + apiKey: data.apiKey, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + instanceId: instanceId, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + splitMessages: data.splitMessages, + timePerChar: data.timePerChar, + }, + }); + + return bot; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating evoai'); + } + } + + public async findBot(instance: InstanceDto) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const bots = await this.botRepository.findMany({ + where: { + instanceId: instanceId, + }, + }); + + if (!bots.length) { + return null; + } + + return bots; + } + + public async fetchBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai 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('Evoai not found'); + } + + if (bot.instanceId !== instanceId) { + throw new Error('Evoai not found'); + } + + return bot; + } + + public async updateBot(instance: InstanceDto, botId: string, data: EvoaiDto) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const bot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!bot) { + throw new Error('Evoai not found'); + } + + if (bot.instanceId !== instanceId) { + throw new Error('Evoai 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 evoai 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, + agentUrl: data.agentUrl, + apiKey: data.apiKey, + }, + }); + + if (checkDuplicate) { + throw new Error('Evoai already exists'); + } + + if (data.triggerType === 'keyword') { + if (!data.triggerOperator || !data.triggerValue) { + throw new Error('Trigger operator and value are required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + id: { not: botId }, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + } + + if (data.triggerType === 'advanced') { + if (!data.triggerValue) { + throw new Error('Trigger value is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerValue: data.triggerValue, + id: { not: botId }, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + } + + try { + const bot = await this.botRepository.update({ + where: { + id: botId, + }, + data: { + enabled: data?.enabled, + description: data.description, + agentUrl: data.agentUrl, + apiKey: data.apiKey, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + instanceId: instanceId, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + splitMessages: data.splitMessages, + timePerChar: data.timePerChar, + }, + }); + + return bot; + } catch (error) { + this.logger.error(error); + throw new Error('Error updating evoai'); + } + } + + public async deleteBot(instance: InstanceDto, botId: string) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai 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('Evoai not found'); + } + + if (bot.instanceId !== instanceId) { + throw new Error('Evoai 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 evoai bot'); + } + } + + // Settings + public async settings(instance: InstanceDto, data: any) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (settings) { + const updateSettings = await this.settingsRepository.update({ + where: { + id: settings.id, + }, + data: { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + evoaiIdFallback: data.evoaiIdFallback, + ignoreJids: data.ignoreJids, + splitMessages: data.splitMessages, + timePerChar: data.timePerChar, + }, + }); + + return { + expire: updateSettings.expire, + keywordFinish: updateSettings.keywordFinish, + delayMessage: updateSettings.delayMessage, + unknownMessage: updateSettings.unknownMessage, + listeningFromMe: updateSettings.listeningFromMe, + stopBotFromMe: updateSettings.stopBotFromMe, + keepOpen: updateSettings.keepOpen, + debounceTime: updateSettings.debounceTime, + evoaiIdFallback: updateSettings.evoaiIdFallback, + ignoreJids: updateSettings.ignoreJids, + splitMessages: updateSettings.splitMessages, + timePerChar: updateSettings.timePerChar, + }; + } + + const newSetttings = await this.settingsRepository.create({ + data: { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + evoaiIdFallback: data.evoaiIdFallback, + ignoreJids: data.ignoreJids, + instanceId: instanceId, + splitMessages: data.splitMessages, + timePerChar: data.timePerChar, + }, + }); + + return { + expire: newSetttings.expire, + keywordFinish: newSetttings.keywordFinish, + delayMessage: newSetttings.delayMessage, + unknownMessage: newSetttings.unknownMessage, + listeningFromMe: newSetttings.listeningFromMe, + stopBotFromMe: newSetttings.stopBotFromMe, + keepOpen: newSetttings.keepOpen, + debounceTime: newSetttings.debounceTime, + evoaiIdFallback: newSetttings.evoaiIdFallback, + ignoreJids: newSetttings.ignoreJids, + splitMessages: newSetttings.splitMessages, + timePerChar: newSetttings.timePerChar, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } + } + + public async fetchSettings(instance: InstanceDto) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + Fallback: true, + }, + }); + + if (!settings) { + return { + expire: 0, + keywordFinish: '', + delayMessage: 0, + unknownMessage: '', + listeningFromMe: false, + stopBotFromMe: false, + keepOpen: false, + ignoreJids: [], + splitMessages: false, + timePerChar: 0, + evoaiIdFallback: '', + fallback: null, + }; + } + + return { + expire: settings.expire, + keywordFinish: settings.keywordFinish, + delayMessage: settings.delayMessage, + unknownMessage: settings.unknownMessage, + listeningFromMe: settings.listeningFromMe, + stopBotFromMe: settings.stopBotFromMe, + keepOpen: settings.keepOpen, + ignoreJids: settings.ignoreJids, + splitMessages: settings.splitMessages, + timePerChar: settings.timePerChar, + evoaiIdFallback: settings.evoaiIdFallback, + fallback: settings.Fallback, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching default settings'); + } + } + + // Sessions + public async changeStatus(instance: InstanceDto, data: any) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const defaultSettingCheck = await this.settingsRepository.findFirst({ + where: { + instanceId, + }, + }); + + const remoteJid = data.remoteJid; + const status = data.status; + + if (status === 'delete') { + await this.sessionRepository.deleteMany({ + where: { + remoteJid: remoteJid, + botId: { not: null }, + }, + }); + + return { bot: { remoteJid: remoteJid, status: status } }; + } + + if (status === 'closed') { + if (defaultSettingCheck?.keepOpen) { + await this.sessionRepository.updateMany({ + where: { + remoteJid: remoteJid, + botId: { not: null }, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.sessionRepository.deleteMany({ + where: { + remoteJid: remoteJid, + botId: { not: null }, + }, + }); + } + + return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } }; + } else { + const session = await this.sessionRepository.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + botId: { not: null }, + }, + data: { + status: status, + }, + }); + + const botData = { + remoteJid: remoteJid, + status: status, + session, + }; + + return { bot: { ...instance, bot: botData } }; + } + } catch (error) { + this.logger.error(error); + throw new Error('Error changing status'); + } + } + + public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const bot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (bot && bot.instanceId !== instanceId) { + throw new Error('Evoai not found'); + } + + return await this.sessionRepository.findMany({ + where: { + instanceId: instanceId, + remoteJid, + botId: bot ? botId : { not: null }, + type: 'evoai', + }, + }); + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching sessions'); + } + } + + public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { + if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled'); + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (!settings) { + throw new Error('Settings not found'); + } + + let ignoreJids: any = settings?.ignoreJids || []; + + if (data.action === 'add') { + if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids }; + + ignoreJids.push(data.remoteJid); + } else { + ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid); + } + + const updateSettings = await this.settingsRepository.update({ + where: { + id: settings.id, + }, + data: { + ignoreJids: ignoreJids, + }, + }); + + return { + ignoreJids: updateSettings.ignoreJids, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } + } + + // Emit + public async emit({ instance, remoteJid, msg }: EmitData) { + if (!this.integrationEnabled) return; + + try { + const settings = await this.settingsRepository.findFirst({ + where: { + instanceId: instance.instanceId, + }, + }); + + if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return; + + const session = await this.getSession(remoteJid, instance); + + const content = getConversationMessage(msg); + + let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvoaiModel; + + if (!findBot) { + const fallback = await this.settingsRepository.findFirst({ + where: { + instanceId: instance.instanceId, + }, + }); + + if (fallback?.evoaiIdFallback) { + const findFallback = await this.botRepository.findFirst({ + where: { + id: fallback.evoaiIdFallback, + }, + }); + + findBot = findFallback; + } else { + return; + } + } + + let expire = findBot?.expire; + let keywordFinish = findBot?.keywordFinish; + let delayMessage = findBot?.delayMessage; + let unknownMessage = findBot?.unknownMessage; + let listeningFromMe = findBot?.listeningFromMe; + let stopBotFromMe = findBot?.stopBotFromMe; + let keepOpen = findBot?.keepOpen; + let debounceTime = findBot?.debounceTime; + let ignoreJids = findBot?.ignoreJids; + let splitMessages = findBot?.splitMessages; + let timePerChar = findBot?.timePerChar; + + if (expire === undefined || expire === null) expire = settings.expire; + if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish; + if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage; + if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage; + if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe; + if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe; + if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen; + if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime; + if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids; + if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false; + if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0; + + const key = msg.key as { + id: string; + remoteJid: string; + fromMe: boolean; + participant: string; + }; + + if (stopBotFromMe && key.fromMe && session) { + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'paused', + }, + }); + return; + } + + if (!listeningFromMe && key.fromMe) { + return; + } + + if (session && !session.awaitUser) { + return; + } + + if (debounceTime && debounceTime > 0) { + this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => { + await this.evoaiService.processEvoai( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + findBot, + session, + { + ...settings, + expire, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, + debounceTime, + ignoreJids, + splitMessages, + timePerChar, + }, + debouncedContent, + msg?.pushName, + msg, + ); + }); + } else { + await this.evoaiService.processEvoai( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + findBot, + session, + { + ...settings, + expire, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, + debounceTime, + ignoreJids, + splitMessages, + timePerChar, + }, + content, + msg?.pushName, + msg, + ); + } + + return; + } catch (error) { + this.logger.error(error); + return; + } + } +} diff --git a/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts b/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts new file mode 100644 index 00000000..30991bbe --- /dev/null +++ b/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts @@ -0,0 +1,37 @@ +import { TriggerOperator, TriggerType } from '@prisma/client'; + +export class EvoaiDto { + enabled?: boolean; + description?: string; + agentUrl?: string; + apiKey?: string; + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + triggerType?: TriggerType; + triggerOperator?: TriggerOperator; + triggerValue?: string; + ignoreJids?: any; + splitMessages?: boolean; + timePerChar?: number; +} + +export class EvoaiSettingDto { + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + evoaiIdFallback?: string; + ignoreJids?: any; + splitMessages?: boolean; + timePerChar?: number; +} diff --git a/src/api/integrations/chatbot/evoai/routes/evoai.router.ts b/src/api/integrations/chatbot/evoai/routes/evoai.router.ts new file mode 100644 index 00000000..aadca01d --- /dev/null +++ b/src/api/integrations/chatbot/evoai/routes/evoai.router.ts @@ -0,0 +1,124 @@ +import { RouterBroker } from '@api/abstract/abstract.router'; +import { IgnoreJidDto } from '@api/dto/chatbot.dto'; +import { InstanceDto } from '@api/dto/instance.dto'; +import { HttpStatus } from '@api/routes/index.router'; +import { evoaiController } from '@api/server.module'; +import { + evoaiIgnoreJidSchema, + evoaiSchema, + evoaiSettingSchema, + evoaiStatusSchema, + instanceSchema, +} from '@validate/validate.schema'; +import { RequestHandler, Router } from 'express'; + +import { EvoaiDto, EvoaiSettingDto } from '../dto/evoai.dto'; + +export class EvoaiRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('create'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: evoaiSchema, + ClassRef: EvoaiDto, + execute: (instance, data) => evoaiController.createBot(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => evoaiController.findBot(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetch/:evoaiId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => evoaiController.fetchBot(instance, req.params.evoaiId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('update/:evoaiId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: evoaiSchema, + ClassRef: EvoaiDto, + execute: (instance, data) => evoaiController.updateBot(instance, req.params.evoaiId, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('delete/:evoaiId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => evoaiController.deleteBot(instance, req.params.evoaiId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('settings'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: evoaiSettingSchema, + ClassRef: EvoaiSettingDto, + execute: (instance, data) => evoaiController.settings(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchSettings'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => evoaiController.fetchSettings(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('changeStatus'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: evoaiStatusSchema, + ClassRef: InstanceDto, + execute: (instance, data) => evoaiController.changeStatus(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchSessions/:evoaiId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => evoaiController.fetchSessions(instance, req.params.evoaiId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: evoaiIgnoreJidSchema, + ClassRef: IgnoreJidDto, + execute: (instance, data) => evoaiController.ignoreJid(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router: Router = Router(); +} diff --git a/src/api/integrations/chatbot/evoai/services/evoai.service.ts b/src/api/integrations/chatbot/evoai/services/evoai.service.ts new file mode 100644 index 00000000..431766a9 --- /dev/null +++ b/src/api/integrations/chatbot/evoai/services/evoai.service.ts @@ -0,0 +1,524 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { InstanceDto } from '@api/dto/instance.dto'; +import { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; +import { Integration } from '@api/types/wa.types'; +import { ConfigService, Language } from '@config/env.config'; +import { Logger } from '@config/logger.config'; +import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client'; +import { sendTelemetry } from '@utils/sendTelemetry'; +import axios from 'axios'; +import { downloadMediaMessage } from 'baileys'; +import FormData from 'form-data'; +import { v4 as uuidv4 } from 'uuid'; + +export class EvoaiService { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly prismaRepository: PrismaRepository, + private readonly configService: ConfigService, + ) {} + + private readonly logger = new Logger('EvoaiService'); + + public async createNewSession(instance: InstanceDto, data: any) { + try { + const session = await this.prismaRepository.integrationSession.create({ + data: { + remoteJid: data.remoteJid, + pushName: data.pushName, + sessionId: data.remoteJid, + status: 'opened', + awaitUser: false, + botId: data.botId, + instanceId: instance.instanceId, + type: 'evoai', + }, + }); + + return { session }; + } catch (error) { + this.logger.error(error); + return; + } + } + + private isImageMessage(content: string) { + return content.includes('imageMessage'); + } + + private isAudioMessage(content: string) { + return content.includes('audioMessage'); + } + + private async speechToText(audioBuffer: Buffer): Promise { + try { + const apiKey = this.configService.get('OPENAI')?.API_KEY; + if (!apiKey) { + this.logger.error('[EvoAI] No OpenAI API key set for Whisper transcription'); + return null; + } + const lang = this.configService.get('LANGUAGE').includes('pt') + ? 'pt' + : this.configService.get('LANGUAGE'); + const formData = new FormData(); + formData.append('file', audioBuffer, 'audio.ogg'); + formData.append('model', 'whisper-1'); + formData.append('language', lang); + const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, { + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${apiKey}`, + }, + }); + return response?.data?.text || null; + } catch (err) { + this.logger.error(`[EvoAI] Whisper transcription failed: ${err}`); + return null; + } + } + + private async sendMessageToBot( + instance: any, + session: IntegrationSession, + settings: EvoaiSetting, + evoai: Evoai, + remoteJid: string, + pushName: string, + content: string, + msg?: any, + ) { + try { + const endpoint: string = evoai.agentUrl; + const callId = `call-${uuidv4()}`; + const taskId = `task-${uuidv4()}`; + + // Prepare message parts + const parts: any[] = [ + { + type: 'text', + text: content, + }, + ]; + + // If content indicates an image/file, fetch and encode as base64, then send as a file part + if ((this.isImageMessage(content) || this.isAudioMessage(content)) && msg) { + const isImage = this.isImageMessage(content); + const isAudio = this.isAudioMessage(content); + this.logger.debug(`[EvoAI] Media message detected: ${content}`); + + let transcribedText = null; + if (isAudio) { + try { + this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`); + const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {}); + transcribedText = await this.speechToText(mediaBuffer); + if (transcribedText) { + parts[0].text = transcribedText; + } else { + parts[0].text = '[Audio message could not be transcribed]'; + } + } catch (err) { + this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`); + parts[0].text = '[Audio message could not be transcribed]'; + } + } else if (isImage) { + const contentSplit = content.split('|'); + parts[0].text = contentSplit[2] || content; + let fileContent = null, + fileName = null, + mimeType = null; + try { + this.logger.debug( + `[EvoAI] Fetching image using downloadMediaMessage with msg.key: ${JSON.stringify(msg.key)}`, + ); + const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {}); + fileContent = Buffer.from(mediaBuffer).toString('base64'); + fileName = contentSplit[2] || `${msg.key.id}.jpg`; + mimeType = 'image/jpeg'; + parts.push({ + type: 'file', + file: { + name: fileName, + bytes: fileContent, + mimeType: mimeType, + }, + }); + } catch (fileErr) { + this.logger.error(`[EvoAI] Failed to fetch or encode image for EvoAI: ${fileErr}`); + } + } + } + + const payload = { + jsonrpc: '2.0', + method: 'tasks/send', + params: { + message: { + role: 'user', + parts, + }, + sessionId: session.sessionId, + id: taskId, + }, + id: callId, + }; + + this.logger.debug(`[EvoAI] Sending request to: ${endpoint}`); + // Redact base64 file bytes from payload log + const redactedPayload = JSON.parse(JSON.stringify(payload)); + if (redactedPayload?.params?.message?.parts) { + redactedPayload.params.message.parts = redactedPayload.params.message.parts.map((part) => { + if (part.type === 'file' && part.file && part.file.bytes) { + return { ...part, file: { ...part.file, bytes: '[base64 omitted]' } }; + } + return part; + }); + } + this.logger.debug(`[EvoAI] Payload: ${JSON.stringify(redactedPayload)}`); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.presenceSubscribe(remoteJid); + await instance.client.sendPresenceUpdate('composing', remoteJid); + } + + const response = await axios.post(endpoint, payload, { + headers: { + 'x-api-key': evoai.apiKey, + 'Content-Type': 'application/json', + }, + }); + + this.logger.debug(`[EvoAI] Response: ${JSON.stringify(response.data.status)}`); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) + await instance.client.sendPresenceUpdate('paused', remoteJid); + + let message = undefined; + const result = response?.data?.result; + if (result?.status?.message?.parts && Array.isArray(result.status.message.parts)) { + const textPart = result.status.message.parts.find((p) => p.type === 'text' && p.text); + if (textPart) message = textPart.text; + } + this.logger.debug(`[EvoAI] Extracted message to send: ${message}`); + const conversationId = session.sessionId; + + if (message) { + await this.sendMessageWhatsApp(instance, remoteJid, message, settings); + } + + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: true, + sessionId: conversationId, + }, + }); + } catch (error) { + this.logger.error( + `[EvoAI] Error sending message: ${error?.response?.data ? JSON.stringify(error.response.data) : error}`, + ); + return; + } + } + + private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: EvoaiSetting) { + const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; + + let textBuffer = ''; + let lastIndex = 0; + + let match: RegExpExecArray | null; + + const getMediaType = (url: string): string | null => { + const extension = url.split('.').pop()?.toLowerCase(); + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + const audioExtensions = ['mp3', 'wav', 'aac', 'ogg']; + const videoExtensions = ['mp4', 'avi', 'mkv', 'mov']; + const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt']; + + if (imageExtensions.includes(extension || '')) return 'image'; + if (audioExtensions.includes(extension || '')) return 'audio'; + if (videoExtensions.includes(extension || '')) return 'video'; + if (documentExtensions.includes(extension || '')) return 'document'; + return null; + }; + + while ((match = linkRegex.exec(message)) !== null) { + const [fullMatch, exclMark, altText, url] = match; + const mediaType = getMediaType(url); + + const beforeText = message.slice(lastIndex, match.index); + if (beforeText) { + textBuffer += beforeText; + } + + if (mediaType) { + const splitMessages = settings.splitMessages ?? false; + const timePerChar = settings.timePerChar ?? 0; + const minDelay = 1000; + const maxDelay = 20000; + + if (textBuffer.trim()) { + if (splitMessages) { + const multipleMessages = textBuffer.trim().split('\n\n'); + + for (let index = 0; index < multipleMessages.length; index++) { + const message = multipleMessages[index]; + + const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.presenceSubscribe(remoteJid); + await instance.client.sendPresenceUpdate('composing', remoteJid); + } + + await new Promise((resolve) => { + setTimeout(async () => { + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: message, + }, + false, + ); + resolve(); + }, delay); + }); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.sendPresenceUpdate('paused', remoteJid); + } + } + } else { + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: textBuffer.trim(), + }, + false, + ); + } + textBuffer = ''; + } + + if (mediaType === 'audio') { + await instance.audioWhatsapp({ + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + audio: url, + caption: altText, + }); + } else { + await instance.mediaMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + mediatype: mediaType, + media: url, + caption: altText, + }, + null, + false, + ); + } + } else { + textBuffer += `[${altText}](${url})`; + } + + lastIndex = linkRegex.lastIndex; + } + + if (lastIndex < message.length) { + const remainingText = message.slice(lastIndex); + if (remainingText.trim()) { + textBuffer += remainingText; + } + } + + const splitMessages = settings.splitMessages ?? false; + const timePerChar = settings.timePerChar ?? 0; + const minDelay = 1000; + const maxDelay = 20000; + + if (textBuffer.trim()) { + if (splitMessages) { + const multipleMessages = textBuffer.trim().split('\n\n'); + + for (let index = 0; index < multipleMessages.length; index++) { + const message = multipleMessages[index]; + + const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.presenceSubscribe(remoteJid); + await instance.client.sendPresenceUpdate('composing', remoteJid); + } + + await new Promise((resolve) => { + setTimeout(async () => { + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: message, + }, + false, + ); + resolve(); + }, delay); + }); + + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.sendPresenceUpdate('paused', remoteJid); + } + } + } else { + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: textBuffer.trim(), + }, + false, + ); + } + } + + sendTelemetry('/message/sendText'); + } + + private async initNewSession( + instance: any, + remoteJid: string, + evoai: Evoai, + settings: EvoaiSetting, + session: IntegrationSession, + content: string, + pushName?: string, + msg?: any, + ) { + const data = await this.createNewSession(instance, { + remoteJid, + pushName, + botId: evoai.id, + }); + + if (data.session) { + session = data.session; + } + + await this.sendMessageToBot(instance, session, settings, evoai, remoteJid, pushName, content, msg); + + return; + } + + public async processEvoai( + instance: any, + remoteJid: string, + evoai: Evoai, + session: IntegrationSession, + settings: EvoaiSetting, + content: string, + pushName?: string, + msg?: any, + ) { + if (session && session.status !== 'opened') { + return; + } + + if (session && settings.expire && settings.expire > 0) { + const now = Date.now(); + + const sessionUpdatedAt = new Date(session.updatedAt).getTime(); + + const diff = now - sessionUpdatedAt; + + const diffInMinutes = Math.floor(diff / 1000 / 60); + + if (diffInMinutes > settings.expire) { + if (settings.keepOpen) { + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.integrationSession.deleteMany({ + where: { + botId: evoai.id, + remoteJid: remoteJid, + }, + }); + } + + await this.initNewSession(instance, remoteJid, evoai, settings, session, content, pushName, msg); + return; + } + } + + if (!session) { + await this.initNewSession(instance, remoteJid, evoai, settings, session, content, pushName, msg); + return; + } + + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: false, + }, + }); + + if (!content) { + if (settings.unknownMessage) { + this.waMonitor.waInstances[instance.instanceName].textMessage( + { + number: remoteJid.split('@')[0], + delay: settings.delayMessage || 1000, + text: settings.unknownMessage, + }, + false, + ); + + sendTelemetry('/message/sendText'); + } + return; + } + + if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { + if (settings.keepOpen) { + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.integrationSession.deleteMany({ + where: { + botId: evoai.id, + remoteJid: remoteJid, + }, + }); + } + return; + } + + await this.sendMessageToBot(instance, session, settings, evoai, remoteJid, pushName, content, msg); + + return; + } +} diff --git a/src/api/integrations/chatbot/evoai/validate/evoai.schema.ts b/src/api/integrations/chatbot/evoai/validate/evoai.schema.ts new file mode 100644 index 00000000..04e1409b --- /dev/null +++ b/src/api/integrations/chatbot/evoai/validate/evoai.schema.ts @@ -0,0 +1,115 @@ +import { JSONSchema7 } from 'json-schema'; +import { v4 } from 'uuid'; + +const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; +}; + +export const evoaiSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean' }, + description: { type: 'string' }, + agentUrl: { type: 'string' }, + apiKey: { type: 'string' }, + triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] }, + triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, + triggerValue: { type: 'string' }, + expire: { type: 'integer' }, + keywordFinish: { type: 'string' }, + delayMessage: { type: 'integer' }, + unknownMessage: { type: 'string' }, + listeningFromMe: { type: 'boolean' }, + stopBotFromMe: { type: 'boolean' }, + keepOpen: { type: 'boolean' }, + debounceTime: { type: 'integer' }, + ignoreJids: { type: 'array', items: { type: 'string' } }, + splitMessages: { type: 'boolean' }, + timePerChar: { type: 'integer' }, + }, + required: ['enabled', 'agentUrl', 'triggerType'], + ...isNotEmpty('enabled', 'agentUrl', 'triggerType'), +}; + +export const evoaiStatusSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + remoteJid: { type: 'string' }, + status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] }, + }, + required: ['remoteJid', 'status'], + ...isNotEmpty('remoteJid', 'status'), +}; + +export const evoaiSettingSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + expire: { type: 'integer' }, + keywordFinish: { type: 'string' }, + delayMessage: { type: 'integer' }, + unknownMessage: { type: 'string' }, + listeningFromMe: { type: 'boolean' }, + stopBotFromMe: { type: 'boolean' }, + keepOpen: { type: 'boolean' }, + debounceTime: { type: 'integer' }, + ignoreJids: { type: 'array', items: { type: 'string' } }, + evoaiIdFallback: { type: 'string' }, + splitMessages: { type: 'boolean' }, + timePerChar: { type: 'integer' }, + }, + required: [ + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + 'splitMessages', + 'timePerChar', + ], + ...isNotEmpty( + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + 'splitMessages', + 'timePerChar', + ), +}; + +export const evoaiIgnoreJidSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + remoteJid: { type: 'string' }, + action: { type: 'string', enum: ['add', 'remove'] }, + }, + required: ['remoteJid', 'action'], + ...isNotEmpty('remoteJid', 'action'), +}; diff --git a/src/api/server.module.ts b/src/api/server.module.ts index ac4d7e91..5070ca06 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -22,6 +22,8 @@ import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/ import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service'; import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller'; import { DifyService } from './integrations/chatbot/dify/services/dify.service'; +import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller'; +import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service'; import { EvolutionBotController } from './integrations/chatbot/evolutionBot/controllers/evolutionBot.controller'; import { EvolutionBotService } from './integrations/chatbot/evolutionBot/services/evolutionBot.service'; import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller'; @@ -132,4 +134,7 @@ export const flowiseController = new FlowiseController(flowiseService, prismaRep const n8nService = new N8nService(waMonitor, prismaRepository); export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor); +const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService); +export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor); + logger.info('Module - ON'); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index c5cac824..b9dd74ef 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -254,6 +254,7 @@ export type Chatwoot = { export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string }; export type Dify = { ENABLED: boolean }; export type N8n = { ENABLED: boolean }; +export type Evoai = { ENABLED: boolean }; export type S3 = { ACCESS_KEY: string; @@ -294,6 +295,7 @@ export interface Env { OPENAI: Openai; DIFY: Dify; N8N: N8n; + EVOAI: Evoai; CACHE: CacheConf; S3?: S3; AUTHENTICATION: Auth; @@ -592,6 +594,9 @@ export class ConfigService { N8N: { ENABLED: process.env?.N8N_ENABLED === 'true', }, + EVOAI: { + ENABLED: process.env?.EVOAI_ENABLED === 'true', + }, CACHE: { REDIS: { ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',