diff --git a/.env.example b/.env.example index 94daa156..401c9020 100644 --- a/.env.example +++ b/.env.example @@ -122,6 +122,8 @@ CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=false OPENAI_ENABLED=false OPENAI_API_KEY_GLOBAL= +DIFY_ENABLED=false + CACHE_REDIS_ENABLED=true CACHE_REDIS_URI=redis://localhost:6379/6 CACHE_REDIS_PREFIX_KEY=evolution diff --git a/package.json b/package.json index a8e43952..065492c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "2.0.4-beta", + "version": "2.0.4-rc", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/api/integrations/dify/controllers/dify.controller.ts b/src/api/integrations/dify/controllers/dify.controller.ts new file mode 100644 index 00000000..ce4e807b --- /dev/null +++ b/src/api/integrations/dify/controllers/dify.controller.ts @@ -0,0 +1,69 @@ +import { configService, Dify } from '../../../../config/env.config'; +import { BadRequestException } from '../../../../exceptions'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { DifyDto, DifyIgnoreJidDto } from '../dto/dify.dto'; +import { DifyService } from '../services/dify.service'; + +export class DifyController { + constructor(private readonly difyService: DifyService) {} + + public async createDify(instance: InstanceDto, data: DifyDto) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.create(instance, data); + } + + public async findDify(instance: InstanceDto) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.find(instance); + } + + public async fetchDify(instance: InstanceDto, difyId: string) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.fetch(instance, difyId); + } + + public async updateDify(instance: InstanceDto, difyId: string, data: DifyDto) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.update(instance, difyId, data); + } + + public async deleteDify(instance: InstanceDto, difyId: string) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.delete(instance, difyId); + } + + public async settings(instance: InstanceDto, data: any) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.setDefaultSettings(instance, data); + } + + public async fetchSettings(instance: InstanceDto) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.fetchDefaultSettings(instance); + } + + public async changeStatus(instance: InstanceDto, data: any) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.changeStatus(instance, data); + } + + public async fetchSessions(instance: InstanceDto, difyId: string) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.fetchSessions(instance, difyId); + } + + public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) { + if (!configService.get('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); + + return this.difyService.ignoreJid(instance, data); + } +} diff --git a/src/api/integrations/dify/dto/dify.dto.ts b/src/api/integrations/dify/dto/dify.dto.ts new file mode 100644 index 00000000..37dcc3f0 --- /dev/null +++ b/src/api/integrations/dify/dto/dify.dto.ts @@ -0,0 +1,46 @@ +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; + botType?: $Enums.DifyBotType; + apiUrl?: string; + apiKey?: string; + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + triggerType?: TriggerType; + triggerOperator?: TriggerOperator; + triggerValue?: string; + ignoreJids?: any; +} + +export class DifySettingDto { + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + difyIdFallback?: string; + ignoreJids?: any; +} + +export class DifyIgnoreJidDto { + remoteJid?: string; + action?: string; +} diff --git a/src/api/integrations/dify/routes/dify.router.ts b/src/api/integrations/dify/routes/dify.router.ts new file mode 100644 index 00000000..f3f61d12 --- /dev/null +++ b/src/api/integrations/dify/routes/dify.router.ts @@ -0,0 +1,123 @@ +import { RequestHandler, Router } from 'express'; + +import { + difyIgnoreJidSchema, + difySchema, + difySettingSchema, + difyStatusSchema, + instanceSchema, +} from '../../../../validate/validate.schema'; +import { RouterBroker } from '../../../abstract/abstract.router'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { HttpStatus } from '../../../routes/index.router'; +import { difyController } from '../../../server.module'; +import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '../dto/dify.dto'; + +export class DifyRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('create'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: difySchema, + ClassRef: DifyDto, + execute: (instance, data) => difyController.createDify(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) => difyController.findDify(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetch/:difyId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => difyController.fetchDify(instance, req.params.difyId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('update/:difyId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: difySchema, + ClassRef: DifyDto, + execute: (instance, data) => difyController.updateDify(instance, req.params.difyId, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('delete/:difyId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => difyController.deleteDify(instance, req.params.difyId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('settings'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: difySettingSchema, + ClassRef: DifySettingDto, + execute: (instance, data) => difyController.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) => difyController.fetchSettings(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('changeStatus'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: difyStatusSchema, + ClassRef: InstanceDto, + execute: (instance, data) => difyController.changeStatus(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchSessions/:difyId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => difyController.fetchSessions(instance, req.params.difyId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: difyIgnoreJidSchema, + ClassRef: DifyIgnoreJidDto, + execute: (instance, data) => difyController.ignoreJid(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/api/integrations/dify/services/dify.service.ts b/src/api/integrations/dify/services/dify.service.ts new file mode 100644 index 00000000..c31115b1 --- /dev/null +++ b/src/api/integrations/dify/services/dify.service.ts @@ -0,0 +1,1321 @@ +import { Dify, DifySession, DifySetting, Message } from '@prisma/client'; +import axios from 'axios'; + +import { ConfigService, S3 } from '../../../../config/env.config'; +import { Logger } from '../../../../config/logger.config'; +import { sendTelemetry } from '../../../../utils/sendTelemetry'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { PrismaRepository } from '../../../repository/repository.service'; +import { WAMonitoringService } from '../../../services/monitor.service'; +import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '../dto/dify.dto'; + +export class DifyService { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly prismaRepository: PrismaRepository, + ) {} + + private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; + + private readonly logger = new Logger(DifyService.name); + + 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'); + } + } + + try { + const dify = await this.prismaRepository.dify.create({ + 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 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: { + DifySession: 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'); + } + + 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: { + DifySession: 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.difySession.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) { + throw new Error('Default settings not found'); + } + + 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) { + throw new Error('Dify not found'); + } + + if (dify.instanceId !== instanceId) { + throw new Error('Dify not found'); + } + + if (dify) { + return await this.prismaRepository.difySession.findMany({ + where: { + difyId: difyId, + }, + }); + } + + if (remoteJid) { + return await this.prismaRepository.difySession.findMany({ + where: { + remoteJid: remoteJid, + difyId: difyId, + }, + }); + } + } 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.difySession.deleteMany({ + where: { + remoteJid: remoteJid, + }, + }); + + return { dify: { remoteJid: remoteJid, status: status } }; + } + + if (status === 'closed') { + if (defaultSettingCheck?.keepOpen) { + await this.prismaRepository.difySession.updateMany({ + where: { + remoteJid: remoteJid, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.difySession.deleteMany({ + where: { + remoteJid: remoteJid, + }, + }); + } + + return { dify: { ...instance, dify: { remoteJid: remoteJid, status: status } } }; + } else { + const session = await this.prismaRepository.difySession.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + }, + 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?.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 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; + + // 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', + triggerValue: { + startsWith: content, + }, + 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', + triggerValue: { + endsWith: content, + }, + 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', + triggerValue: { + contains: content, + }, + 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; + } + } + + const session = await this.prismaRepository.difySession.findFirst({ + where: { + remoteJid: remoteJid, + }, + }); + + 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) { + if (keepOpen) { + await this.prismaRepository.difySession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.difySession.deleteMany({ + where: { + difyId: findDify.id, + remoteJid: remoteJid, + }, + }); + } + 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, + ); + }); + } else { + await this.processDify( + this.waMonitor.waInstances[instance.instanceName], + remoteJid, + findDify, + session, + settings, + content, + ); + } + + return; + } catch (error) { + this.logger.error(error); + return; + } + } + + public async createNewSession(instance: InstanceDto, data: any) { + try { + const session = await this.prismaRepository.difySession.create({ + data: { + remoteJid: data.remoteJid, + sessionId: data.remoteJid, + status: 'opened', + awaitUser: false, + difyId: data.difyId, + instanceId: instance.instanceId, + }, + }); + + return { session }; + } catch (error) { + this.logger.error(error); + return; + } + } + + private async initNewSession( + instance: any, + remoteJid: string, + dify: Dify, + settings: DifySetting, + session: DifySession, + content: string, + ) { + const data = await this.createNewSession(instance, { + remoteJid, + difyId: dify.id, + }); + + if (data.session) { + session = data.session; + } + + let endpoint: string = dify.apiUrl; + let payload: any = {}; + + if (dify.botType === 'chatBot') { + endpoint += '/chat-messages'; + payload = { + inputs: {}, + query: content, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'textGenerator') { + endpoint += '/completion-messages'; + payload = { + inputs: { + query: content, + }, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'agent') { + endpoint += '/chat-messages'; + payload = { + inputs: {}, + query: content, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'workflow') { + endpoint += '/workflows/run'; + payload = { + inputs: { + query: content, + }, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + await instance.client.presenceSubscribe(remoteJid); + + await instance.client.sendPresenceUpdate('composing', remoteJid); + + const response = await axios.post(endpoint, payload, { + headers: { + Authorization: `Bearer ${dify.apiKey}`, + }, + }); + + await instance.client.sendPresenceUpdate('paused', remoteJid); + + const message = response?.data?.answer; + + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: message, + }, + false, + ); + + await this.prismaRepository.difySession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: true, + sessionId: response?.data?.conversation_id, + }, + }); + + sendTelemetry('/message/sendText'); + + return; + } + + private async processDify( + instance: any, + remoteJid: string, + dify: Dify, + session: DifySession, + settings: DifySetting, + content: string, + ) { + if (session && session.status !== 'opened') { + return; + } + + if (session && settings.expire && settings.expire > 0) { + const now = Date.now(); + + const sessionUpdatedAt = new Date(session.updatedAt).getTime(); + + const diff = now - sessionUpdatedAt; + + const diffInMinutes = Math.floor(diff / 1000 / 60); + + if (diffInMinutes > settings.expire) { + if (settings.keepOpen) { + await this.prismaRepository.difySession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.difySession.deleteMany({ + where: { + difyId: dify.id, + remoteJid: remoteJid, + }, + }); + } + + await this.initNewSession(instance, remoteJid, dify, settings, session, content); + return; + } + } + + if (!session) { + await this.initNewSession(instance, remoteJid, dify, settings, session, content); + return; + } + + await this.prismaRepository.difySession.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.difySession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } else { + await this.prismaRepository.difySession.deleteMany({ + where: { + difyId: dify.id, + remoteJid: remoteJid, + }, + }); + } + return; + } + + let endpoint: string = dify.apiUrl; + let payload: any = {}; + + if (dify.botType === 'chatBot') { + endpoint += '/chat-messages'; + payload = { + inputs: {}, + query: content, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'textGenerator') { + endpoint += '/completion-messages'; + payload = { + inputs: { + query: content, + }, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'agent') { + endpoint += '/chat-messages'; + payload = { + inputs: {}, + query: content, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + if (dify.botType === 'workflow') { + endpoint += '/workflows/run'; + payload = { + inputs: { + query: content, + }, + response_mode: 'blocking', + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, + }; + } + + await instance.client.presenceSubscribe(remoteJid); + + await instance.client.sendPresenceUpdate('composing', remoteJid); + + const response = await axios.post(endpoint, payload, { + headers: { + Authorization: `Bearer ${dify.apiKey}`, + }, + }); + + await instance.client.sendPresenceUpdate('paused', remoteJid); + + const message = response?.data?.answer; + + await instance.textMessage( + { + number: remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + text: message, + }, + false, + ); + + await this.prismaRepository.difySession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: true, + }, + }); + + sendTelemetry('/message/sendText'); + + return; + } +} diff --git a/src/api/integrations/dify/validate/dify.schema.ts b/src/api/integrations/dify/validate/dify.schema.ts new file mode 100644 index 00000000..43e79149 --- /dev/null +++ b/src/api/integrations/dify/validate/dify.schema.ts @@ -0,0 +1,107 @@ +import { JSONSchema7 } from 'json-schema'; +import { v4 } from 'uuid'; + +const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; +}; + +export const difySchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean' }, + botType: { type: 'string', enum: ['chatBot', 'textGenerator', 'agent', 'workflow'] }, + apiUrl: { type: 'string' }, + apiKey: { type: 'string' }, + triggerType: { type: 'string', enum: ['all', 'keyword', 'none'] }, + triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, + triggerValue: { type: 'string' }, + expire: { type: 'integer' }, + keywordFinish: { type: 'string' }, + delayMessage: { type: 'integer' }, + unknownMessage: { type: 'string' }, + listeningFromMe: { type: 'boolean' }, + stopBotFromMe: { type: 'boolean' }, + keepOpen: { type: 'boolean' }, + debounceTime: { type: 'integer' }, + ignoreJids: { type: 'array', items: { type: 'string' } }, + }, + required: ['enabled', 'botType', 'triggerType'], + ...isNotEmpty('enabled', 'botType', 'triggerType'), +}; + +export const difyStatusSchema: 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 difySettingSchema: 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' } }, + difyIdFallback: { type: 'string' }, + }, + required: [ + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + ], + ...isNotEmpty( + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + ), +}; + +export const difyIgnoreJidSchema: 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/integrations/openai/services/openai.service.ts b/src/api/integrations/openai/services/openai.service.ts index 0041d67d..580ed9a1 100644 --- a/src/api/integrations/openai/services/openai.service.ts +++ b/src/api/integrations/openai/services/openai.service.ts @@ -730,7 +730,7 @@ export class OpenaiService { }) .then((instance) => instance.id); - const defaultSettingCheck = await this.prismaRepository.typebotSetting.findFirst({ + const defaultSettingCheck = await this.prismaRepository.openaiSetting.findFirst({ where: { instanceId, }, diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 82863a57..506fe777 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -8,6 +8,7 @@ import { authGuard } from '../guards/auth.guard'; import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard'; import Telemetry from '../guards/telemetry.guard'; import { ChatwootRouter } from '../integrations/chatwoot/routes/chatwoot.router'; +import { DifyRouter } from '../integrations/dify/routes/dify.router'; import { OpenaiRouter } from '../integrations/openai/routes/openai.router'; import { RabbitmqRouter } from '../integrations/rabbitmq/routes/rabbitmq.router'; import { S3Router } from '../integrations/s3/routes/s3.router'; @@ -88,6 +89,7 @@ router .use('/label', new LabelRouter(...guards).router) .use('/s3', new S3Router(...guards).router) .use('/openai', new OpenaiRouter(...guards).router) + .use('/dify', new DifyRouter(...guards).router) .get('/webhook/meta', async (req, res) => { if (req.query['hub.verify_token'] === configService.get('WA_BUSINESS').TOKEN_WEBHOOK) res.send(req.query['hub.challenge']); diff --git a/src/api/server.module.ts b/src/api/server.module.ts index d26c1800..0e9ae391 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -13,6 +13,8 @@ import { TemplateController } from './controllers/template.controller'; import { WebhookController } from './controllers/webhook.controller'; import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service'; +import { DifyController } from './integrations/dify/controllers/dify.controller'; +import { DifyService } from './integrations/dify/services/dify.service'; import { OpenaiController } from './integrations/openai/controllers/openai.controller'; import { OpenaiService } from './integrations/openai/services/openai.service'; import { RabbitmqController } from './integrations/rabbitmq/controllers/rabbitmq.controller'; @@ -70,6 +72,9 @@ export const typebotController = new TypebotController(typebotService); const openaiService = new OpenaiService(waMonitor, configService, prismaRepository); export const openaiController = new OpenaiController(openaiService); +const difyService = new DifyService(waMonitor, configService, prismaRepository); +export const difyController = new DifyController(difyService); + const s3Service = new S3Service(prismaRepository); export const s3Controller = new S3Controller(s3Service); diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 6584ecd4..d00ccd54 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -26,6 +26,7 @@ import { SettingsDto } from '../dto/settings.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { ChatwootDto } from '../integrations/chatwoot/dto/chatwoot.dto'; import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service'; +import { DifyService } from '../integrations/dify/services/dify.service'; import { OpenaiService } from '../integrations/openai/services/openai.service'; import { RabbitmqDto } from '../integrations/rabbitmq/dto/rabbitmq.dto'; import { getAMQP, removeQueues } from '../integrations/rabbitmq/libs/amqp.server'; @@ -71,6 +72,8 @@ export class ChannelStartupService { public openaiService = new OpenaiService(waMonitor, this.configService, this.prismaRepository); + public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository); + public setInstance(instance: InstanceDto) { this.logger.setInstance(instance.instanceName); diff --git a/src/api/services/channels/whatsapp.baileys.service.ts b/src/api/services/channels/whatsapp.baileys.service.ts index 946ae1ba..983de7e4 100644 --- a/src/api/services/channels/whatsapp.baileys.service.ts +++ b/src/api/services/channels/whatsapp.baileys.service.ts @@ -68,6 +68,7 @@ import { configService, ConfigSessionPhone, Database, + Dify, Log, Openai, ProviderSession, @@ -1187,6 +1188,17 @@ export class BaileysStartupService extends ChannelStartupService { } } + if (this.configService.get('DIFY').ENABLED) { + if (type === 'notify') { + if (messageRaw.messageType !== 'reactionMessage') + await this.difyService.sendDify( + { instanceName: this.instance.name, instanceId: this.instanceId }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + } + const contact = await this.prismaRepository.contact.findFirst({ where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId }, }); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index c6ec77ea..26ff40a0 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -192,6 +192,7 @@ export type Chatwoot = { }; }; export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string }; +export type Dify = { ENABLED: boolean }; export type S3 = { ACCESS_KEY: string; @@ -226,6 +227,7 @@ export interface Env { TYPEBOT: Typebot; CHATWOOT: Chatwoot; OPENAI: Openai; + DIFY: Dify; CACHE: CacheConf; S3?: S3; AUTHENTICATION: Auth; @@ -437,6 +439,9 @@ export class ConfigService { ENABLED: process.env?.OPENAI_ENABLED === 'true', API_KEY_GLOBAL: process.env?.OPENAI_API_KEY_GLOBAL || null, }, + DIFY: { + ENABLED: process.env?.DIFY_ENABLED === 'true', + }, CACHE: { REDIS: { ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 38967aa3..9321718a 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -1,5 +1,6 @@ // Integrations Schema export * from '../api/integrations/chatwoot/validate/chatwoot.schema'; +export * from '../api/integrations/dify/validate/dify.schema'; export * from '../api/integrations/openai/validate/openai.schema'; export * from '../api/integrations/rabbitmq/validate/rabbitmq.schema'; export * from '../api/integrations/sqs/validate/sqs.schema';