Merge branch 'ev2' into v2.0.0

This commit is contained in:
Stênio Aníbal 2024-08-21 14:56:55 -03:00
commit 4aac2da253
35 changed files with 4952 additions and 4124 deletions

View File

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

View File

@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "GenericBot" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"description" VARCHAR(255),
"apiUrl" VARCHAR(255),
"apiKey" VARCHAR(255),
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"triggerType" "TriggerType",
"triggerOperator" "TriggerOperator",
"triggerValue" TEXT,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "GenericBot_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GenericSetting" (
"id" TEXT NOT NULL,
"expire" INTEGER DEFAULT 0,
"keywordFinish" VARCHAR(100),
"delayMessage" INTEGER,
"unknownMessage" VARCHAR(100),
"listeningFromMe" BOOLEAN DEFAULT false,
"stopBotFromMe" BOOLEAN DEFAULT false,
"keepOpen" BOOLEAN DEFAULT false,
"debounceTime" INTEGER,
"ignoreJids" JSONB,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"botIdFallback" VARCHAR(100),
"instanceId" TEXT NOT NULL,
CONSTRAINT "GenericSetting_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "GenericSetting_instanceId_key" ON "GenericSetting"("instanceId");
-- AddForeignKey
ALTER TABLE "GenericBot" ADD CONSTRAINT "GenericBot_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_botIdFallback_fkey" FOREIGN KEY ("botIdFallback") REFERENCES "GenericBot"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import { chatbotController } from '@api/server.module';
import { CacheService } from '@api/services/cache.service'; import { CacheService } from '@api/services/cache.service';
import { ChannelStartupService } from '@api/services/channel.service'; import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types'; import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Database, Dify, Openai, S3, Typebot, WaBusiness } from '@config/env.config'; import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { BadRequestException, InternalServerErrorException } from '@exceptions';
import axios from 'axios'; import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator'; import { arrayUnique, isURL } from 'class-validator';
@ -923,35 +923,13 @@ export class BusinessStartupService extends ChannelStartupService {
); );
} }
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration) { if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot.enabled && isIntegration)
if (this.configService.get<Typebot>('TYPEBOT').ENABLED) { await chatbotController.emit({
if (messageRaw.messageType !== 'reactionMessage') instance: { instanceName: this.instance.name, instanceId: this.instanceId },
await this.typebotService.sendTypebot( remoteJid: messageRaw.key.remoteJid,
{ instanceName: this.instance.name, instanceId: this.instanceId }, msg: messageRaw,
messageRaw.key.remoteJid, pushName: messageRaw.pushName,
messageRaw, });
);
}
if (this.configService.get<Openai>('OPENAI').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.openaiService.sendOpenai(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw.pushName,
messageRaw,
);
}
if (this.configService.get<Dify>('DIFY').ENABLED) {
if (messageRaw.messageType !== 'reactionMessage')
await this.difyService.sendDify(
{ instanceName: this.instance.name, instanceId: this.instanceId },
messageRaw.key.remoteJid,
messageRaw,
);
}
}
await this.prismaRepository.message.create({ await this.prismaRepository.message.create({
data: messageRaw, data: messageRaw,

View File

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

View File

@ -4,6 +4,8 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router'; import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
import { Router } from 'express'; import { Router } from 'express';
import { GenericRouter } from './generic/routes/generic.router';
export class ChatbotRouter { export class ChatbotRouter {
public readonly router: Router; public readonly router: Router;
@ -14,5 +16,6 @@ export class ChatbotRouter {
this.router.use('/typebot', new TypebotRouter(...guards).router); this.router.use('/typebot', new TypebotRouter(...guards).router);
this.router.use('/openai', new OpenaiRouter(...guards).router); this.router.use('/openai', new OpenaiRouter(...guards).router);
this.router.use('/dify', new DifyRouter(...guards).router); this.router.use('/dify', new DifyRouter(...guards).router);
this.router.use('/generic', new GenericRouter(...guards).router);
} }
} }

View File

@ -1,4 +1,5 @@
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema'; export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
export * from '@api/integrations/chatbot/dify/validate/dify.schema'; export * from '@api/integrations/chatbot/dify/validate/dify.schema';
export * from '@api/integrations/chatbot/generic/validate/generic.schema';
export * from '@api/integrations/chatbot/openai/validate/openai.schema'; export * from '@api/integrations/chatbot/openai/validate/openai.schema';
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema'; export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';

View File

@ -1,84 +1,825 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { DifyDto, DifyIgnoreJidDto } from '@api/integrations/chatbot/dify/dto/dify.dto'; import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service'; import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Dify } from '@config/env.config'; import { configService, Dify } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions'; import { BadRequestException } from '@exceptions';
import { getConversationMessage } from '@utils/getConversationMessage';
export class DifyController { import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
constructor(private readonly difyService: DifyService) {}
public async createDify(instance: InstanceDto, data: DifyDto) { export class DifyController extends ChatbotController implements ChatbotControllerInterface {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); constructor(
private readonly difyService: DifyService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
return this.difyService.create(instance, data); this.botRepository = this.prismaRepository.dify;
this.settingsRepository = this.prismaRepository.difySetting;
this.sessionRepository = this.prismaRepository.integrationSession;
} }
public async findDify(instance: InstanceDto) { public readonly logger = new Logger(DifyController.name);
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
return this.difyService.find(instance); integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data.enabled,
description: data.description,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating dify');
}
} }
public async fetchDify(instance: InstanceDto, difyId: string) { public async findBot(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetch(instance, difyId); const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
} }
public async updateDify(instance: InstanceDto, difyId: string, data: DifyDto) { public async fetchBot(instance: InstanceDto, botId: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.update(instance, difyId, data); const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return bot;
} }
public async deleteDify(instance: InstanceDto, difyId: string) { public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.delete(instance, difyId); const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data.enabled,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating dify');
}
} }
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting dify bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) { public async settings(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.setDefaultSettings(instance, data); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
difyIdFallback: updateSettings.difyIdFallback,
ignoreJids: updateSettings.ignoreJids,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
difyIdFallback: newSetttings.difyIdFallback,
ignoreJids: newSetttings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
} }
public async fetchSettings(instance: InstanceDto) { public async fetchSettings(instance: InstanceDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchDefaultSettings(instance); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
difyIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
difyIdFallback: settings.difyIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
} }
// Sessions
public async changeStatus(instance: InstanceDto, data: any) { public async changeStatus(instance: InstanceDto, data: any) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.changeStatus(instance, data); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
} }
public async fetchSessions(instance: InstanceDto, difyId: string) { public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.fetchSessions(instance, difyId); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
} }
public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) { public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled'); if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
return this.difyService.ignoreJid(instance, data); try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
} }
public async emit({ // Emit
instance, public async emit({ instance, remoteJid, msg }: EmitData) {
remoteJid, if (!this.integrationEnabled) return;
msg,
}: {
instance: InstanceDto;
remoteJid: string;
msg: any;
pushName?: string;
}) {
if (!configService.get<Dify>('DIFY').ENABLED) return;
await this.difyService.sendDify(instance, remoteJid, msg); try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
const findBot = await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
);
if (!findBot) return;
let expire = findBot.expire;
let keywordFinish = findBot.keywordFinish;
let delayMessage = findBot.delayMessage;
let unknownMessage = findBot.unknownMessage;
let listeningFromMe = findBot.listeningFromMe;
let stopBotFromMe = findBot.stopBotFromMe;
let keepOpen = findBot.keepOpen;
let debounceTime = findBot.debounceTime;
if (
!expire ||
!keywordFinish ||
!delayMessage ||
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen ||
!debounceTime
) {
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (!debounceTime) debounceTime = settings.debounceTime;
}
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.difyService.processDify(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
debouncedContent,
msg?.pushName,
);
});
} else {
await this.difyService.processDify(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
} }
} }

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,806 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { getConversationMessage } from '@utils/getConversationMessage';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { GenericBotDto } from '../dto/generic.dto';
import { GenericService } from '../services/generic.service';
export class GenericController extends ChatbotController implements ChatbotControllerInterface {
constructor(
private readonly genericService: GenericService,
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.botRepository = this.prismaRepository.genericBot;
this.settingsRepository = this.prismaRepository.genericSetting;
this.sessionRepository = this.prismaRepository.integrationSession;
}
public readonly logger = new Logger(GenericController.name);
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Bots
public async createBot(instance: InstanceDto, data: GenericBotDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
if (
!data.expire ||
!data.keywordFinish ||
!data.delayMessage ||
!data.unknownMessage ||
!data.listeningFromMe ||
!data.stopBotFromMe ||
!data.keepOpen ||
!data.debounceTime ||
!data.ignoreJids
) {
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!defaultSettingCheck) {
await this.settings(instance, {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
ignoreJids: data.ignoreJids,
});
}
}
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
instanceId: instanceId,
},
});
if (checkTriggerAll && data.triggerType === 'all') {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data.enabled,
description: data.description,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating bot');
}
}
public async findBot(instance: InstanceDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
if (!bots.length) {
return null;
}
return bots;
}
public async fetchBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
return bot;
}
public async updateBot(instance: InstanceDto, botId: string, data: GenericBotDto) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
}
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Bot already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data.enabled,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating bot');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Bot not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Bot not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
botIdFallback: data.botIdFallback,
ignoreJids: data.ignoreJids,
},
});
return {
expire: updateSettings.expire,
keywordFinish: updateSettings.keywordFinish,
delayMessage: updateSettings.delayMessage,
unknownMessage: updateSettings.unknownMessage,
listeningFromMe: updateSettings.listeningFromMe,
stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime,
botIdFallback: updateSettings.botIdFallback,
ignoreJids: updateSettings.ignoreJids,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
botIdFallback: data.botIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
botIdFallback: newSetttings.botIdFallback,
ignoreJids: newSetttings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
public async fetchSettings(instance: InstanceDto) {
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
botIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
botIdFallback: settings.botIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Emit
public async emit({ instance, remoteJid, msg }: EmitData) {
if (!this.integrationEnabled) return;
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
const findBot = await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
);
if (!findBot) return;
let expire = findBot.expire;
let keywordFinish = findBot.keywordFinish;
let delayMessage = findBot.delayMessage;
let unknownMessage = findBot.unknownMessage;
let listeningFromMe = findBot.listeningFromMe;
let stopBotFromMe = findBot.stopBotFromMe;
let keepOpen = findBot.keepOpen;
let debounceTime = findBot.debounceTime;
if (
!expire ||
!keywordFinish ||
!delayMessage ||
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen ||
!debounceTime
) {
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (!debounceTime) debounceTime = settings.debounceTime;
}
const key = msg.key as {
id: string;
remoteJid: string;
fromMe: boolean;
participant: string;
};
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
if (!listeningFromMe && key.fromMe) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.genericService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
debouncedContent,
msg?.pushName,
);
});
} else {
await this.genericService.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
settings,
content,
msg?.pushName,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,107 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const genericSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
apiUrl: { type: 'string' },
apiKey: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
triggerValue: { type: 'string' },
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
},
required: ['enabled', 'apiUrl', 'triggerType'],
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
};
export const genericStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
},
required: ['remoteJid', 'status'],
...isNotEmpty('remoteJid', 'status'),
};
export const genericSettingSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
expire: { type: 'integer' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'integer' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' },
ignoreJids: { type: 'array', items: { type: 'string' } },
botIdFallback: { type: 'string' },
},
required: [
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
],
...isNotEmpty(
'expire',
'keywordFinish',
'delayMessage',
'unknownMessage',
'listeningFromMe',
'stopBotFromMe',
'keepOpen',
'debounceTime',
'ignoreJids',
),
};
export const genericIgnoreJidSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
...isNotEmpty('remoteJid', 'action'),
};

View File

@ -1,13 +1,5 @@
import { TriggerOperator, TriggerType } from '@prisma/client'; import { TriggerOperator, TriggerType } from '@prisma/client';
export class Session {
remoteJid?: string;
sessionId?: string;
status?: string;
createdAt?: number;
updateAt?: number;
}
export class OpenaiCredsDto { export class OpenaiCredsDto {
name: string; name: string;
apiKey: string; apiKey: string;
@ -53,8 +45,3 @@ export class OpenaiSettingDto {
ignoreJids?: any; ignoreJids?: any;
speechToText?: boolean; speechToText?: boolean;
} }
export class OpenaiIgnoreJidDto {
remoteJid?: string;
action?: string;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,22 @@
import { PrismaRepository } from '@api/repository/repository.service'; import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types'; import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook, Websocket } from '@config/env.config'; import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { BadRequestException, NotFoundException } from '@exceptions'; import { BadRequestException, NotFoundException } from '@exceptions';
import axios from 'axios'; import axios from 'axios';
import { isURL } from 'class-validator'; import { isURL } from 'class-validator';
import { EventController } from '../../event.controller'; import { EmitData, EventController, EventControllerInterface } from '../../event.controller';
import { WebhookDto } from '../dto/webhook.dto'; import { WebhookDto } from '../dto/webhook.dto';
export class WebhookController extends EventController { export class WebhookController extends EventController implements EventControllerInterface {
private readonly logger = new Logger(WebhookController.name); private readonly logger = new Logger(WebhookController.name);
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor); super(prismaRepository, waMonitor);
} }
integrationEnabled: boolean;
public async set(instanceName: string, data: WebhookDto): Promise<wa.LocalWebHook> { public async set(instanceName: string, data: WebhookDto): Promise<wa.LocalWebHook> {
if (!isURL(data.url, { require_tld: false })) { if (!isURL(data.url, { require_tld: false })) {
@ -31,7 +32,7 @@ export class WebhookController extends EventController {
} }
await this.get(instanceName); await this.get(instanceName);
return this.prisma.webhook.upsert({ return this.prisma.webhook.upsert({
where: { where: {
instanceId: this.monitor.waInstances[instanceName].instanceId, instanceId: this.monitor.waInstances[instanceName].instanceId,
@ -78,23 +79,13 @@ export class WebhookController extends EventController {
sender, sender,
apiKey, apiKey,
local, local,
}: { }: EmitData): Promise<void> {
instanceName: string; const instanceWebhook = await this.get(instanceName);
origin: string; if (!instanceWebhook || !instanceWebhook.enabled) {
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
local?: boolean;
}): Promise<void> {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
return; return;
} }
const instanceWebhook = await this.get(instanceName); const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookGlobal = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instanceWebhook?.events; const webhookLocal = instanceWebhook?.events;
const we = event.replace(/[.-]/gm, '_').toUpperCase(); const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase();
@ -110,6 +101,7 @@ export class WebhookController extends EventController {
server_url: serverUrl, server_url: serverUrl,
apikey: apiKey, apikey: apiKey,
}; };
if (local) { if (local) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string; let baseURL: string;
@ -153,16 +145,12 @@ export class WebhookController extends EventController {
} }
} }
if (webhookGlobal.GLOBAL?.ENABLED) { if (webhookConfig.GLOBAL?.ENABLED) {
if (webhookGlobal.EVENTS[we]) { if (webhookConfig.EVENTS[we]) {
const globalWebhook = configService.get<Webhook>('WEBHOOK').GLOBAL; let globalURL = webhookConfig.GLOBAL.URL;
let globalURL; if (webhookConfig.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalURL}/${transformedWe}`;
if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
} }
if (enabledLog) { if (enabledLog) {
@ -176,7 +164,7 @@ export class WebhookController extends EventController {
} }
try { try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL }); const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', webhookData); await httpService.post('', webhookData);

View File

@ -1,7 +1,7 @@
import { RouterBroker } from '@api/abstract/abstract.router'; import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto'; import { InstanceDto } from '@api/dto/instance.dto';
import { webhookController } from '@api/server.module';
import { HttpStatus } from '@api/routes/index.router'; import { HttpStatus } from '@api/routes/index.router';
import { webhookController } from '@api/server.module';
import { ConfigService, WaBusiness } from '@config/env.config'; import { ConfigService, WaBusiness } from '@config/env.config';
import { instanceSchema, webhookSchema } from '@validate/validate.schema'; import { instanceSchema, webhookSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express'; import { RequestHandler, Router } from 'express';

View File

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

View File

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

View File

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

View File

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