From a5d72a0dfd3735a84f9ecdff01d2ea261a745944 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 30 Jul 2024 13:34:35 -0300 Subject: [PATCH] feat: Integration with Dify Adds support for Dify integration, including new routes, services, and controllers. The configuration for Dify has been added to the environment file, and the necessary changes have been made to the `.env.example` file. Additionally, the WhatsApp Baileys service has been updated to handle Dify notifications. Modified files: - `.env.example` - `package.json` - `src/api/integrations/openai/services/openai.service.ts` - `src/api/routes/index.router.ts` - `src/api/server.module.ts` - `src/api/services/channel.service.ts` - `src/api/services/channels/whatsapp.baileys.service.ts` - `src/config/env.config.ts` - `src/validate/validate.schema.ts` Introduced files: - `src/api/integrations/dify/` - `src/api/integrations/dify/controllers/dify.controller.ts` - `src/api/integrations/dify/dto/dify.dto.ts` - `src/api/integrations/dify/routes/dify.router.ts` - `src/api/integrations/dify/services/dify.service.ts` - `src/api/integrations/dify/validate/dify.schema.ts` --- .env.example | 2 + package.json | 2 +- .../dify/controllers/dify.controller.ts | 69 + src/api/integrations/dify/dto/dify.dto.ts | 46 + .../integrations/dify/routes/dify.router.ts | 123 ++ .../dify/services/dify.service.ts | 1321 +++++++++++++++++ .../integrations/dify/validate/dify.schema.ts | 107 ++ .../openai/services/openai.service.ts | 2 +- src/api/routes/index.router.ts | 2 + src/api/server.module.ts | 5 + src/api/services/channel.service.ts | 3 + .../channels/whatsapp.baileys.service.ts | 12 + src/config/env.config.ts | 5 + src/validate/validate.schema.ts | 1 + 14 files changed, 1698 insertions(+), 2 deletions(-) create mode 100644 src/api/integrations/dify/controllers/dify.controller.ts create mode 100644 src/api/integrations/dify/dto/dify.dto.ts create mode 100644 src/api/integrations/dify/routes/dify.router.ts create mode 100644 src/api/integrations/dify/services/dify.service.ts create mode 100644 src/api/integrations/dify/validate/dify.schema.ts 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';