mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-10 18:39:38 -06:00
refactor: remove 6 chatbot integrations keeping only N8N
Removed chatbot integrations: Chatwoot, Typebot, OpenAI, Dify, Flowise, and EvoAI. Only N8N integration remains active. Changes include: - Deleted all integration directories and files (controllers, services, DTOs, validators, routers) - Updated chatbot.controller.ts to only emit to N8N - Updated chatbot.router.ts and chatbot.schema.ts to export only N8N - Removed OpenAI dependency from N8nService (removed audio transcription) - Updated server.module.ts to remove all chatbot service instantiations - Cleaned monitor.service.ts and channel.service.ts from chatbot references - Removed chatbot properties from DTOs and validation schemas - Removed LocalChatwoot type and TYPEBOT events from wa.types - Cleaned PostgreSQL Prisma schema: removed 12 models and 2 enums - Removed chatbot relations from Instance model - Removed Chatwoot fields from Message model N8N remains as the only supported chatbot integration.
This commit is contained in:
parent
8884ef42d0
commit
2606dbdac3
@ -48,18 +48,6 @@ enum TriggerOperator {
|
||||
regex
|
||||
}
|
||||
|
||||
enum OpenaiBotType {
|
||||
assistant
|
||||
chatCompletion
|
||||
}
|
||||
|
||||
enum DifyBotType {
|
||||
chatBot
|
||||
textGenerator
|
||||
agent
|
||||
workflow
|
||||
}
|
||||
|
||||
model Instance {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
|
||||
@ -48,18 +48,6 @@ enum TriggerOperator {
|
||||
regex
|
||||
}
|
||||
|
||||
enum OpenaiBotType {
|
||||
assistant
|
||||
chatCompletion
|
||||
}
|
||||
|
||||
enum DifyBotType {
|
||||
chatBot
|
||||
textGenerator
|
||||
agent
|
||||
workflow
|
||||
}
|
||||
|
||||
model Instance {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
@ -81,7 +69,6 @@ model Instance {
|
||||
Contact Contact[]
|
||||
Message Message[]
|
||||
Webhook Webhook?
|
||||
Chatwoot Chatwoot?
|
||||
Label Label[]
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
@ -90,25 +77,14 @@ model Instance {
|
||||
Sqs Sqs?
|
||||
Kafka Kafka?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
Session Session?
|
||||
MessageUpdate MessageUpdate[]
|
||||
TypebotSetting TypebotSetting?
|
||||
Media Media[]
|
||||
OpenaiCreds OpenaiCreds[]
|
||||
OpenaiBot OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
IntegrationSession IntegrationSession[]
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
@ -159,11 +135,6 @@ model Message {
|
||||
contextInfo Json? @db.JsonB
|
||||
source DeviceMessage
|
||||
messageTimestamp Int @db.Integer
|
||||
chatwootMessageId Int? @db.Integer
|
||||
chatwootInboxId Int? @db.Integer
|
||||
chatwootConversationId Int? @db.Integer
|
||||
chatwootContactInboxSourceId String? @db.VarChar(100)
|
||||
chatwootIsRead Boolean? @db.Boolean
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
MessageUpdate MessageUpdate[]
|
||||
@ -210,31 +181,6 @@ model Webhook {
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Chatwoot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean? @default(true) @db.Boolean
|
||||
accountId String? @db.VarChar(100)
|
||||
token String? @db.VarChar(100)
|
||||
url String? @db.VarChar(500)
|
||||
nameInbox String? @db.VarChar(100)
|
||||
signMsg Boolean? @default(false) @db.Boolean
|
||||
signDelimiter String? @db.VarChar(100)
|
||||
number String? @db.VarChar(100)
|
||||
reopenConversation Boolean? @default(false) @db.Boolean
|
||||
conversationPending Boolean? @default(false) @db.Boolean
|
||||
mergeBrazilContacts Boolean? @default(false) @db.Boolean
|
||||
importContacts Boolean? @default(false) @db.Boolean
|
||||
importMessages Boolean? @default(false) @db.Boolean
|
||||
daysLimitImportMessages Int? @db.Integer
|
||||
organization String? @db.VarChar(100)
|
||||
logo String? @db.VarChar(500)
|
||||
ignoreJids Json?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Label {
|
||||
id String @id @default(cuid())
|
||||
labelId String? @db.VarChar(100)
|
||||
@ -346,54 +292,6 @@ model Pusher {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Typebot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
url String @db.VarChar(500)
|
||||
typebot String @db.VarChar(100)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
TypebotSetting TypebotSetting[]
|
||||
}
|
||||
|
||||
model TypebotSetting {
|
||||
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
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @db.VarChar(500)
|
||||
@ -406,53 +304,6 @@ model Media {
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model OpenaiCreds {
|
||||
id String @id @default(cuid())
|
||||
name String? @unique @db.VarChar(255)
|
||||
apiKey String? @unique @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiAssistant OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
}
|
||||
|
||||
model OpenaiBot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType OpenaiBotType
|
||||
assistantId String? @db.VarChar(255)
|
||||
functionUrl String? @db.VarChar(500)
|
||||
model String? @db.VarChar(100)
|
||||
systemMessages Json? @db.JsonB
|
||||
assistantMessages Json? @db.JsonB
|
||||
userMessages Json? @db.JsonB
|
||||
maxTokens Int? @db.Integer
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
|
||||
openaiCredsId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiSetting OpenaiSetting[]
|
||||
}
|
||||
|
||||
model IntegrationSession {
|
||||
id String @id @default(cuid())
|
||||
sessionId String @db.VarChar(255)
|
||||
@ -472,30 +323,6 @@ model IntegrationSession {
|
||||
botId String?
|
||||
}
|
||||
|
||||
model OpenaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
speechToText Boolean? @default(false) @db.Boolean
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id])
|
||||
openaiCredsId String @unique
|
||||
Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id])
|
||||
openaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Template {
|
||||
id String @id @default(cuid())
|
||||
templateId String @unique @db.VarChar(255)
|
||||
@ -508,103 +335,6 @@ model Template {
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model Dify {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType DifyBotType
|
||||
apiUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
DifySetting DifySetting[]
|
||||
}
|
||||
|
||||
model DifySetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Dify? @relation(fields: [difyIdFallback], references: [id])
|
||||
difyIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Flowise {
|
||||
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?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
FlowiseSetting FlowiseSetting[]
|
||||
}
|
||||
|
||||
model FlowiseSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Flowise? @relation(fields: [flowiseIdFallback], references: [id])
|
||||
flowiseIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model IsOnWhatsapp {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @unique @db.VarChar(100)
|
||||
@ -662,51 +392,3 @@ model N8nSetting {
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
@ -37,20 +37,6 @@ export class InstanceDto extends IntegrationDto {
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
};
|
||||
chatwootAccountId?: string;
|
||||
chatwootConversationPending?: boolean;
|
||||
chatwootAutoCreate?: boolean;
|
||||
chatwootDaysLimitImportMessages?: number;
|
||||
chatwootImportContacts?: boolean;
|
||||
chatwootImportMessages?: boolean;
|
||||
chatwootLogo?: string;
|
||||
chatwootMergeBrazilContacts?: boolean;
|
||||
chatwootNameInbox?: string;
|
||||
chatwootOrganization?: string;
|
||||
chatwootReopenConversation?: boolean;
|
||||
chatwootSignMsg?: boolean;
|
||||
chatwootToken?: string;
|
||||
chatwootUrl?: string;
|
||||
}
|
||||
|
||||
export class SetPresenceDto {
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import {
|
||||
difyController,
|
||||
evoaiController,
|
||||
flowiseController,
|
||||
n8nController,
|
||||
openaiController,
|
||||
typebotController,
|
||||
} from '@api/server.module';
|
||||
import { n8nController } from '@api/server.module';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { IntegrationSession } from '@prisma/client';
|
||||
@ -91,17 +84,7 @@ export class ChatbotController {
|
||||
isIntegration,
|
||||
};
|
||||
|
||||
typebotController.emit(emitData);
|
||||
|
||||
openaiController.emit(emitData);
|
||||
|
||||
difyController.emit(emitData);
|
||||
|
||||
n8nController.emit(emitData);
|
||||
|
||||
evoaiController.emit(emitData);
|
||||
|
||||
flowiseController.emit(emitData);
|
||||
}
|
||||
|
||||
public processDebounce(
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
import { ChatwootRouter } from '@api/integrations/chatbot/chatwoot/routes/chatwoot.router';
|
||||
import { DifyRouter } from '@api/integrations/chatbot/dify/routes/dify.router';
|
||||
import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.router';
|
||||
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
import { EvoaiRouter } from './evoai/routes/evoai.router';
|
||||
import { FlowiseRouter } from './flowise/routes/flowise.router';
|
||||
import { N8nRouter } from './n8n/routes/n8n.router';
|
||||
|
||||
export class ChatbotRouter {
|
||||
@ -14,12 +8,6 @@ export class ChatbotRouter {
|
||||
constructor(...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.router.use('/chatwoot', new ChatwootRouter(...guards).router);
|
||||
this.router.use('/typebot', new TypebotRouter(...guards).router);
|
||||
this.router.use('/openai', new OpenaiRouter(...guards).router);
|
||||
this.router.use('/dify', new DifyRouter(...guards).router);
|
||||
this.router.use('/flowise', new FlowiseRouter(...guards).router);
|
||||
this.router.use('/n8n', new N8nRouter(...guards).router);
|
||||
this.router.use('/evoai', new EvoaiRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1 @@
|
||||
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
|
||||
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
|
||||
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
|
||||
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
|
||||
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
|
||||
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
|
||||
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { waMonitor } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { CacheEngine } from '@cache/cacheengine';
|
||||
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
export class ChatwootController {
|
||||
constructor(
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
if (data?.enabled) {
|
||||
if (!isURL(data.url, { require_tld: false })) {
|
||||
throw new BadRequestException('url is not valid');
|
||||
}
|
||||
|
||||
if (!data.accountId) {
|
||||
throw new BadRequestException('accountId is required');
|
||||
}
|
||||
|
||||
if (!data.token) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
if (data.signMsg !== true && data.signMsg !== false) {
|
||||
throw new BadRequestException('signMsg is required');
|
||||
}
|
||||
if (data.signMsg === false) data.signDelimiter = null;
|
||||
}
|
||||
|
||||
if (!data.nameInbox || data.nameInbox === '') {
|
||||
data.nameInbox = instance.instanceName;
|
||||
}
|
||||
|
||||
const result = await this.chatwootService.create(instance, data);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async findChatwoot(instance: InstanceDto): Promise<ChatwootDto & { webhook_url: string }> {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
const result = await this.chatwootService.find(instance);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
if (Object.keys(result || {}).length === 0) {
|
||||
return {
|
||||
enabled: false,
|
||||
url: '',
|
||||
accountId: '',
|
||||
token: '',
|
||||
signMsg: false,
|
||||
nameInbox: '',
|
||||
webhook_url: '',
|
||||
};
|
||||
}
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
|
||||
const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache);
|
||||
|
||||
return chatwootService.receiveWebhook(instance, data);
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import { Constructor } from '@api/integrations/integration.dto';
|
||||
|
||||
export class ChatwootDto {
|
||||
enabled?: boolean;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
nameInbox?: string;
|
||||
signMsg?: boolean;
|
||||
signDelimiter?: string;
|
||||
number?: string;
|
||||
reopenConversation?: boolean;
|
||||
conversationPending?: boolean;
|
||||
mergeBrazilContacts?: boolean;
|
||||
importContacts?: boolean;
|
||||
importMessages?: boolean;
|
||||
daysLimitImportMessages?: number;
|
||||
autoCreate?: boolean;
|
||||
organization?: string;
|
||||
logo?: string;
|
||||
ignoreJids?: string[];
|
||||
}
|
||||
|
||||
export function ChatwootInstanceMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
chatwootAccountId?: string;
|
||||
chatwootToken?: string;
|
||||
chatwootUrl?: string;
|
||||
chatwootSignMsg?: boolean;
|
||||
chatwootReopenConversation?: boolean;
|
||||
chatwootConversationPending?: boolean;
|
||||
chatwootMergeBrazilContacts?: boolean;
|
||||
chatwootImportContacts?: boolean;
|
||||
chatwootImportMessages?: boolean;
|
||||
chatwootDaysLimitImportMessages?: number;
|
||||
chatwootNameInbox?: string;
|
||||
chatwootOrganization?: string;
|
||||
chatwootLogo?: string;
|
||||
chatwootAutoCreate?: boolean;
|
||||
};
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import { Chatwoot, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import postgresql from 'pg';
|
||||
|
||||
const { Pool } = postgresql;
|
||||
|
||||
class Postgres {
|
||||
private logger = new Logger('Postgres');
|
||||
private pool;
|
||||
private connected = false;
|
||||
|
||||
getConnection(connectionString: string) {
|
||||
if (this.connected) {
|
||||
return this.pool;
|
||||
} else {
|
||||
this.pool = new Pool({
|
||||
connectionString,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.pool.on('error', () => {
|
||||
this.logger.error('postgres disconnected');
|
||||
this.connected = false;
|
||||
});
|
||||
|
||||
try {
|
||||
this.connected = true;
|
||||
} catch (e) {
|
||||
this.connected = false;
|
||||
this.logger.error('postgres connect exception caught: ' + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pool;
|
||||
}
|
||||
}
|
||||
|
||||
getChatwootConnection() {
|
||||
const uri = configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
|
||||
|
||||
return this.getConnection(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export const postgresClient = new Postgres();
|
||||
@ -1,46 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { chatwootController } from '@api/server.module';
|
||||
import { chatwootSchema, instanceSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class ChatwootRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ChatwootDto>({
|
||||
request: req,
|
||||
schema: chatwootSchema,
|
||||
ClassRef: ChatwootDto,
|
||||
execute: (instance, data) => chatwootController.createChatwoot(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) => chatwootController.findChatwoot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('webhook'), async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => chatwootController.receiveWebhook(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,579 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { Chatwoot, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { inbox } from '@figuro/chatwoot-sdk';
|
||||
import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client';
|
||||
import { proto } from 'baileys';
|
||||
|
||||
type ChatwootUser = {
|
||||
user_type: string;
|
||||
user_id: number;
|
||||
};
|
||||
|
||||
type FksChatwoot = {
|
||||
phone_number: string;
|
||||
contact_id: string;
|
||||
conversation_id: string;
|
||||
};
|
||||
|
||||
type firstLastTimestamp = {
|
||||
first: number;
|
||||
last: number;
|
||||
};
|
||||
|
||||
type IWebMessageInfo = Omit<proto.IWebMessageInfo, 'key'> & Partial<Pick<proto.IWebMessageInfo, 'key'>>;
|
||||
|
||||
class ChatwootImport {
|
||||
private logger = new Logger('ChatwootImport');
|
||||
private repositoryMessagesCache = new Map<string, Set<string>>();
|
||||
private historyMessages = new Map<string, Message[]>();
|
||||
private historyContacts = new Map<string, Contact[]>();
|
||||
|
||||
public getRepositoryMessagesCache(instance: InstanceDto) {
|
||||
return this.repositoryMessagesCache.has(instance.instanceName)
|
||||
? this.repositoryMessagesCache.get(instance.instanceName)
|
||||
: null;
|
||||
}
|
||||
|
||||
public setRepositoryMessagesCache(instance: InstanceDto, repositoryMessagesCache: Set<string>) {
|
||||
this.repositoryMessagesCache.set(instance.instanceName, repositoryMessagesCache);
|
||||
}
|
||||
|
||||
public deleteRepositoryMessagesCache(instance: InstanceDto) {
|
||||
this.repositoryMessagesCache.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public addHistoryMessages(instance: InstanceDto, messagesRaw: Message[]) {
|
||||
const actualValue = this.historyMessages.has(instance.instanceName)
|
||||
? this.historyMessages.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyMessages.set(instance.instanceName, [...actualValue, ...messagesRaw]);
|
||||
}
|
||||
|
||||
public addHistoryContacts(instance: InstanceDto, contactsRaw: Contact[]) {
|
||||
const actualValue = this.historyContacts.has(instance.instanceName)
|
||||
? this.historyContacts.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyContacts.set(instance.instanceName, actualValue.concat(contactsRaw));
|
||||
}
|
||||
|
||||
public deleteHistoryMessages(instance: InstanceDto) {
|
||||
this.historyMessages.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public deleteHistoryContacts(instance: InstanceDto) {
|
||||
this.historyContacts.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public clearAll(instance: InstanceDto) {
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteHistoryContacts(instance);
|
||||
}
|
||||
|
||||
public getHistoryMessagesLenght(instance: InstanceDto) {
|
||||
return this.historyMessages.get(instance.instanceName)?.length ?? 0;
|
||||
}
|
||||
|
||||
public async importHistoryContacts(instance: InstanceDto, provider: ChatwootDto) {
|
||||
try {
|
||||
if (this.getHistoryMessagesLenght(instance) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
let totalContactsImported = 0;
|
||||
|
||||
const contacts = this.historyContacts.get(instance.instanceName) || [];
|
||||
if (contacts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let contactsChunk: Contact[] = this.sliceIntoChunks(contacts, 3000);
|
||||
while (contactsChunk.length > 0) {
|
||||
const labelSql = `SELECT id FROM labels WHERE title = '${provider.nameInbox}' AND account_id = ${provider.accountId} LIMIT 1`;
|
||||
|
||||
let labelId = (await pgClient.query(labelSql))?.rows[0]?.id;
|
||||
|
||||
if (!labelId) {
|
||||
// creating label in chatwoot db and getting the id
|
||||
const sqlLabel = `INSERT INTO labels (title, color, show_on_sidebar, account_id, created_at, updated_at) VALUES ('${provider.nameInbox}', '#34039B', true, ${provider.accountId}, NOW(), NOW()) RETURNING id`;
|
||||
|
||||
labelId = (await pgClient.query(sqlLabel))?.rows[0]?.id;
|
||||
}
|
||||
|
||||
// inserting contacts in chatwoot db
|
||||
let sqlInsert = `INSERT INTO contacts
|
||||
(name, phone_number, account_id, identifier, created_at, updated_at) VALUES `;
|
||||
const bindInsert = [provider.accountId];
|
||||
|
||||
for (const contact of contactsChunk) {
|
||||
const isGroup = this.isIgnorePhoneNumber(contact.remoteJid);
|
||||
|
||||
const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName;
|
||||
bindInsert.push(contactName);
|
||||
const bindName = `$${bindInsert.length}`;
|
||||
|
||||
let bindPhoneNumber: string;
|
||||
if (!isGroup) {
|
||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||
bindPhoneNumber = `$${bindInsert.length}`;
|
||||
} else {
|
||||
bindPhoneNumber = 'NULL';
|
||||
}
|
||||
bindInsert.push(contact.remoteJid);
|
||||
const bindIdentifier = `$${bindInsert.length}`;
|
||||
|
||||
sqlInsert += `(${bindName}, ${bindPhoneNumber}, $1, ${bindIdentifier}, NOW(), NOW()),`;
|
||||
}
|
||||
if (sqlInsert.slice(-1) === ',') {
|
||||
sqlInsert = sqlInsert.slice(0, -1);
|
||||
}
|
||||
sqlInsert += ` ON CONFLICT (identifier, account_id)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
phone_number = EXCLUDED.phone_number,
|
||||
identifier = EXCLUDED.identifier`;
|
||||
|
||||
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
||||
|
||||
const sqlTags = `SELECT id FROM tags WHERE name = '${provider.nameInbox}' LIMIT 1`;
|
||||
|
||||
const tagData = (await pgClient.query(sqlTags))?.rows[0];
|
||||
let tagId = tagData?.id;
|
||||
|
||||
const sqlTag = `INSERT INTO tags (name, taggings_count) VALUES ('${provider.nameInbox}', ${totalContactsImported}) ON CONFLICT (name) DO UPDATE SET taggings_count = tags.taggings_count + ${totalContactsImported} RETURNING id`;
|
||||
|
||||
tagId = (await pgClient.query(sqlTag))?.rows[0]?.id;
|
||||
|
||||
await pgClient.query(sqlTag);
|
||||
|
||||
let sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at) VALUES `;
|
||||
|
||||
contactsChunk.forEach((contact) => {
|
||||
const bindTaggableId = `(SELECT id FROM contacts WHERE identifier = '${contact.remoteJid}' AND account_id = ${provider.accountId})`;
|
||||
sqlInsertLabel += `($1, $2, ${bindTaggableId}, $3, NOW()),`;
|
||||
});
|
||||
|
||||
if (sqlInsertLabel.slice(-1) === ',') {
|
||||
sqlInsertLabel = sqlInsertLabel.slice(0, -1);
|
||||
}
|
||||
|
||||
await pgClient.query(sqlInsertLabel, [tagId, 'Contact', 'labels']);
|
||||
|
||||
contactsChunk = this.sliceIntoChunks(contacts, 3000);
|
||||
}
|
||||
|
||||
this.deleteHistoryContacts(instance);
|
||||
|
||||
return totalContactsImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history contacts: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise<Set<string>> {
|
||||
try {
|
||||
const existingSourceIdsSet = new Set<string>();
|
||||
|
||||
if (sourceIds.length === 0) {
|
||||
return existingSourceIdsSet;
|
||||
}
|
||||
|
||||
// Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds];
|
||||
|
||||
const query = conversationId
|
||||
? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'
|
||||
: 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
|
||||
const result = await pgClient.query(query, params);
|
||||
for (const row of result.rows) {
|
||||
existingSourceIdsSet.add(row.source_id);
|
||||
}
|
||||
|
||||
return existingSourceIdsSet;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`);
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public async importHistoryMessages(
|
||||
instance: InstanceDto,
|
||||
chatwootService: ChatwootService,
|
||||
inbox: inbox,
|
||||
provider: ChatwootModel,
|
||||
) {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const chatwootUser = await this.getChatwootUser(provider);
|
||||
if (!chatwootUser) {
|
||||
throw new Error('User not found to import messages.');
|
||||
}
|
||||
|
||||
let totalMessagesImported = 0;
|
||||
|
||||
let messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
||||
if (messagesOrdered.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ordering messages by number and timestamp asc
|
||||
messagesOrdered.sort((a, b) => {
|
||||
const aKey = a.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
const bKey = b.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
const aMessageTimestamp = a.messageTimestamp as any as number;
|
||||
const bMessageTimestamp = b.messageTimestamp as any as number;
|
||||
|
||||
return parseInt(aKey.remoteJid) - parseInt(bKey.remoteJid) || aMessageTimestamp - bMessageTimestamp;
|
||||
});
|
||||
|
||||
const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered);
|
||||
// Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number}
|
||||
const phoneNumbersWithTimestamp = new Map<string, firstLastTimestamp>();
|
||||
allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => {
|
||||
phoneNumbersWithTimestamp.set(phoneNumber, {
|
||||
first: messages[0]?.messageTimestamp as any as number,
|
||||
last: messages[messages.length - 1]?.messageTimestamp as any as number,
|
||||
});
|
||||
});
|
||||
|
||||
const existingSourceIds = await this.getExistingSourceIds(messagesOrdered.map((message: any) => message.key.id));
|
||||
messagesOrdered = messagesOrdered.filter((message: any) => !existingSourceIds.has(message.key.id));
|
||||
// processing messages in batch
|
||||
const batchSize = 4000;
|
||||
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
while (messagesChunk.length > 0) {
|
||||
// Map structure: +552199999999 => Message[]
|
||||
const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk);
|
||||
|
||||
if (messagesByPhoneNumber.size > 0) {
|
||||
const fksByNumber = await this.selectOrCreateFksFromChatwoot(
|
||||
provider,
|
||||
inbox,
|
||||
phoneNumbersWithTimestamp,
|
||||
messagesByPhoneNumber,
|
||||
);
|
||||
|
||||
// inserting messages in chatwoot db
|
||||
let sqlInsertMsg = `INSERT INTO messages
|
||||
(content, processed_message_content, account_id, inbox_id, conversation_id, message_type, private, content_type,
|
||||
sender_type, sender_id, source_id, created_at, updated_at) VALUES `;
|
||||
const bindInsertMsg = [provider.accountId, inbox.id];
|
||||
|
||||
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
|
||||
const fksChatwoot = fksByNumber.get(phoneNumber);
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (!message.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentMessage = this.getContentMessage(chatwootService, message);
|
||||
if (!contentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindInsertMsg.push(contentMessage);
|
||||
const bindContent = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(fksChatwoot.conversation_id);
|
||||
const bindConversationId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? '1' : '0');
|
||||
const bindMessageType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_type : 'Contact');
|
||||
const bindSenderType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id);
|
||||
const bindSenderId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push('WAID:' + message.key.id);
|
||||
const bindSourceId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.messageTimestamp as number);
|
||||
const bindmessageTimestamp = `$${bindInsertMsg.length}`;
|
||||
|
||||
sqlInsertMsg += `(${bindContent}, ${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0,
|
||||
${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
|
||||
});
|
||||
});
|
||||
if (bindInsertMsg.length > 2) {
|
||||
if (sqlInsertMsg.slice(-1) === ',') {
|
||||
sqlInsertMsg = sqlInsertMsg.slice(0, -1);
|
||||
}
|
||||
totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0;
|
||||
}
|
||||
}
|
||||
messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
}
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
|
||||
const providerData: ChatwootDto = {
|
||||
...provider,
|
||||
ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [],
|
||||
};
|
||||
|
||||
this.importHistoryContacts(instance, providerData);
|
||||
|
||||
return totalMessagesImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history messages: ${error.toString()}`);
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async selectOrCreateFksFromChatwoot(
|
||||
provider: ChatwootModel,
|
||||
inbox: inbox,
|
||||
phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>,
|
||||
messagesByPhoneNumber: Map<string, Message[]>,
|
||||
): Promise<Map<string, FksChatwoot>> {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const bindValues = [provider.accountId, inbox.id];
|
||||
const phoneNumberBind = Array.from(messagesByPhoneNumber.keys())
|
||||
.map((phoneNumber) => {
|
||||
const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber);
|
||||
|
||||
if (phoneNumberTimestamp) {
|
||||
bindValues.push(phoneNumber);
|
||||
let bindStr = `($${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.first);
|
||||
bindStr += `$${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.last);
|
||||
return `${bindStr}$${bindValues.length})`;
|
||||
}
|
||||
})
|
||||
.join(',');
|
||||
|
||||
// select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db
|
||||
const sqlFromChatwoot = `WITH
|
||||
phone_number AS (
|
||||
SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM (
|
||||
VALUES
|
||||
${phoneNumberBind}
|
||||
) as t (phone_number, created_at, last_activity_at)
|
||||
),
|
||||
|
||||
only_new_phone_number AS (
|
||||
SELECT * FROM phone_number
|
||||
WHERE phone_number NOT IN (
|
||||
SELECT phone_number
|
||||
FROM contacts
|
||||
JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id
|
||||
AND con.account_id = $1
|
||||
AND con.inbox_id = $2
|
||||
AND con.contact_id = contacts.id
|
||||
WHERE contacts.account_id = $1
|
||||
)
|
||||
),
|
||||
|
||||
new_contact AS (
|
||||
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
|
||||
SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''),
|
||||
'@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at)
|
||||
FROM only_new_phone_number AS p
|
||||
ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, phone_number, created_at, updated_at
|
||||
),
|
||||
|
||||
new_contact_inbox AS (
|
||||
INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at)
|
||||
SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at
|
||||
FROM new_contact
|
||||
RETURNING id, contact_id, created_at, updated_at
|
||||
),
|
||||
|
||||
new_conversation AS (
|
||||
INSERT INTO conversations (account_id, inbox_id, status, contact_id,
|
||||
contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
||||
SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(),
|
||||
new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at
|
||||
FROM new_contact_inbox
|
||||
RETURNING id, contact_id
|
||||
)
|
||||
|
||||
SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id
|
||||
FROM new_conversation
|
||||
JOIN new_contact ON new_conversation.contact_id = new_contact.id
|
||||
|
||||
UNION
|
||||
|
||||
SELECT p.phone_number, c.id contact_id, con.id conversation_id
|
||||
FROM phone_number p
|
||||
JOIN contacts c ON c.phone_number = p.phone_number
|
||||
JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1
|
||||
AND con.inbox_id = $2 AND con.contact_id = c.id`;
|
||||
|
||||
const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues);
|
||||
|
||||
return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item]));
|
||||
}
|
||||
|
||||
public async getChatwootUser(provider: ChatwootModel): Promise<ChatwootUser> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sqlUser = `SELECT owner_type AS user_type, owner_id AS user_id
|
||||
FROM access_tokens
|
||||
WHERE token = $1`;
|
||||
|
||||
return (await pgClient.query(sqlUser, [provider.token]))?.rows[0] || false;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getChatwootUser: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public createMessagesMapByPhoneNumber(messages: Message[]): Map<string, Message[]> {
|
||||
return messages.reduce((acc: Map<string, Message[]>, message: Message) => {
|
||||
const key = message?.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
if (!this.isIgnorePhoneNumber(key?.remoteJid)) {
|
||||
const phoneNumber = key?.remoteJid?.split('@')[0];
|
||||
if (phoneNumber) {
|
||||
const phoneNumberPlus = `+${phoneNumber}`;
|
||||
const messages = acc.has(phoneNumberPlus) ? acc.get(phoneNumberPlus) : [];
|
||||
messages.push(message);
|
||||
acc.set(phoneNumberPlus, messages);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
public async getContactsOrderByRecentConversations(
|
||||
inbox: inbox,
|
||||
provider: ChatwootModel,
|
||||
limit = 50,
|
||||
): Promise<{ id: number; phone_number: string; identifier: string }[]> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sql = `SELECT contacts.id, contacts.identifier, contacts.phone_number
|
||||
FROM conversations
|
||||
JOIN contacts ON contacts.id = conversations.contact_id
|
||||
WHERE conversations.account_id = $1
|
||||
AND inbox_id = $2
|
||||
ORDER BY conversations.last_activity_at DESC
|
||||
LIMIT $3`;
|
||||
|
||||
return (await pgClient.query(sql, [provider.accountId, inbox.id, limit]))?.rows;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on get recent conversations: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getContentMessage(chatwootService: ChatwootService, msg: IWebMessageInfo) {
|
||||
const contentMessage = chatwootService.getConversationMessage(msg.message);
|
||||
if (contentMessage) {
|
||||
return contentMessage;
|
||||
}
|
||||
|
||||
if (!configService.get<Chatwoot>('CHATWOOT').IMPORT.PLACEHOLDER_MEDIA_MESSAGE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const types = {
|
||||
documentMessage: msg.message.documentMessage,
|
||||
documentWithCaptionMessage: msg.message.documentWithCaptionMessage?.message?.documentMessage,
|
||||
imageMessage: msg.message.imageMessage,
|
||||
videoMessage: msg.message.videoMessage,
|
||||
audioMessage: msg.message.audioMessage,
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
|
||||
switch (typeKey) {
|
||||
case 'documentMessage': {
|
||||
const doc = msg.message.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'documentWithCaptionMessage': {
|
||||
const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'templateMessage': {
|
||||
const template = msg.message.templateMessage?.hydratedTemplate;
|
||||
return (
|
||||
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
|
||||
(template?.hydratedContentText || '')
|
||||
);
|
||||
}
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
||||
case 'videoMessage':
|
||||
return '_<Video Message>_';
|
||||
|
||||
case 'audioMessage':
|
||||
return '_<Audio Message>_';
|
||||
|
||||
case 'stickerMessage':
|
||||
return '_<Sticker Message>_';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public sliceIntoChunks(arr: any[], chunkSize: number) {
|
||||
return arr.splice(0, chunkSize);
|
||||
}
|
||||
|
||||
public isGroup(remoteJid: string) {
|
||||
return remoteJid.includes('@g.us');
|
||||
}
|
||||
|
||||
public isIgnorePhoneNumber(remoteJid: string) {
|
||||
return this.isGroup(remoteJid) || remoteJid === 'status@broadcast' || remoteJid === '0@s.whatsapp.net';
|
||||
}
|
||||
|
||||
public updateMessageSourceID(messageId: string | number, sourceId: string) {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sql = `UPDATE messages SET source_id = $1, status = 0, created_at = NOW(), updated_at = NOW() WHERE id = $2;`;
|
||||
|
||||
return pgClient.query(sql, [`WAID:${sourceId}`, messageId]);
|
||||
}
|
||||
}
|
||||
|
||||
export const chatwootImport = new ChatwootImport();
|
||||
@ -1,45 +0,0 @@
|
||||
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 chatwootSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
accountId: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
signMsg: { type: 'boolean', enum: [true, false] },
|
||||
signDelimiter: { type: ['string', 'null'] },
|
||||
nameInbox: { type: ['string', 'null'] },
|
||||
reopenConversation: { type: 'boolean', enum: [true, false] },
|
||||
conversationPending: { type: 'boolean', enum: [true, false] },
|
||||
autoCreate: { type: 'boolean', enum: [true, false] },
|
||||
importContacts: { type: 'boolean', enum: [true, false] },
|
||||
mergeBrazilContacts: { type: 'boolean', enum: [true, false] },
|
||||
importMessages: { type: 'boolean', enum: [true, false] },
|
||||
daysLimitImportMessages: { type: 'number' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'],
|
||||
...isNotEmpty('enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'),
|
||||
};
|
||||
@ -1,126 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Dify } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
constructor(
|
||||
private readonly difyService: DifyService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.dify;
|
||||
this.settingsRepository = this.prismaRepository.difySetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('DifyController');
|
||||
protected readonly integrationName = 'Dify';
|
||||
|
||||
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'difyIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add Dify-specific validation
|
||||
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);
|
||||
|
||||
// Dify-specific duplicate check
|
||||
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');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process Dify-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: DifyModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
import { $Enums } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class DifyDto extends BaseChatbotDto {
|
||||
botType?: $Enums.DifyBotType;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class DifySettingDto extends BaseChatbotSettingDto {
|
||||
difyIdFallback?: string;
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto, DifySettingDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { difyController } from '@api/server.module';
|
||||
import {
|
||||
difyIgnoreJidSchema,
|
||||
difySchema,
|
||||
difySettingSchema,
|
||||
difyStatusSchema,
|
||||
instanceSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class DifyRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifyDto>({
|
||||
request: req,
|
||||
schema: difySchema,
|
||||
ClassRef: DifyDto,
|
||||
execute: (instance, data) => difyController.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) => difyController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchBot(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifyDto>({
|
||||
request: req,
|
||||
schema: difySchema,
|
||||
ClassRef: DifyDto,
|
||||
execute: (instance, data) => difyController.updateBot(instance, req.params.difyId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.deleteBot(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifySettingDto>({
|
||||
request: req,
|
||||
schema: difySettingSchema,
|
||||
ClassRef: DifySettingDto,
|
||||
execute: (instance, data) => difyController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: difyStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => difyController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchSessions(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: difyIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => difyController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
@ -1,320 +0,0 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'DifyService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for Dify
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
dify: Dify,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
let endpoint: string = dify.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Dify endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
let processedContent = content;
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dify.botType === 'chatBot') {
|
||||
endpoint += '/chat-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: processedContent,
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
let mediaBase64 = msg.message.base64 || null;
|
||||
|
||||
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||
}
|
||||
|
||||
if (mediaBase64) {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: mediaBase64,
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: media[1].split('?')[0],
|
||||
},
|
||||
];
|
||||
}
|
||||
payload.query = media[2] || content;
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (dify.botType === 'textGenerator') {
|
||||
endpoint += '/completion-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: processedContent,
|
||||
pushName: pushName,
|
||||
remoteJid: remoteJid,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
let mediaBase64 = msg.message.base64 || null;
|
||||
|
||||
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||
}
|
||||
|
||||
if (mediaBase64) {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: mediaBase64,
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: media[1].split('?')[0],
|
||||
},
|
||||
];
|
||||
payload.inputs.query = media[2] || content;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (dify.botType === 'agent') {
|
||||
endpoint += '/chat-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: processedContent,
|
||||
response_mode: 'streaming',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: msg.message.mediaUrl || msg.message.base64,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: media[1].split('?')[0],
|
||||
},
|
||||
];
|
||||
payload.query = media[2] || content;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
let conversationId;
|
||||
let answer = '';
|
||||
|
||||
const data = response.data.replaceAll('data: ', '');
|
||||
const events = data.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
for (const eventString of events) {
|
||||
if (eventString.trim().startsWith('{')) {
|
||||
const event = JSON.parse(eventString);
|
||||
|
||||
if (event?.event === 'agent_message') {
|
||||
console.log('event:', event);
|
||||
conversationId = conversationId ?? event?.conversation_id;
|
||||
answer += event?.answer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
if (answer) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings, true);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.response?.data || error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const difySchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
botType: { type: 'string', enum: ['chatBot', 'textGenerator', 'agent', 'workflow'] },
|
||||
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' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'botType', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'botType', 'triggerType'),
|
||||
};
|
||||
|
||||
export const difyStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const difySettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
difyIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const difyIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@ -1,122 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto';
|
||||
import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Evoai } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto> {
|
||||
constructor(
|
||||
private readonly evoaiService: EvoaiService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.evoai;
|
||||
this.settingsRepository = this.prismaRepository.evoaiSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvoaiController');
|
||||
protected readonly integrationName = 'Evoai';
|
||||
|
||||
integrationEnabled = configService.get<Evoai>('EVOAI').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.evoaiIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'evoaiIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: EvoaiDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add EvoAI-specific validation
|
||||
public async createBot(instance: InstanceDto, data: EvoaiDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// EvoAI-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process Evoai-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvoaiModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvoaiDto extends BaseChatbotDto {
|
||||
agentUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class EvoaiSettingDto extends BaseChatbotSettingDto {
|
||||
evoaiIdFallback?: string;
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { evoaiController } from '@api/server.module';
|
||||
import {
|
||||
evoaiIgnoreJidSchema,
|
||||
evoaiSchema,
|
||||
evoaiSettingSchema,
|
||||
evoaiStatusSchema,
|
||||
instanceSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { EvoaiDto, EvoaiSettingDto } from '../dto/evoai.dto';
|
||||
|
||||
export class EvoaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.createBot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.updateBot(instance, req.params.evoaiId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.deleteBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiSettingDto>({
|
||||
request: req,
|
||||
schema: evoaiSettingSchema,
|
||||
ClassRef: EvoaiSettingDto,
|
||||
execute: (instance, data) => evoaiController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: evoaiStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => evoaiController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSessions(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: evoaiIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => evoaiController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
@ -1,207 +0,0 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { downloadMediaMessage } from 'baileys';
|
||||
import { isURL } from 'class-validator';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvoaiService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for EvoAI
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the abstract method to send message to EvoAI API
|
||||
* Handles audio transcription, image processing, and complex JSON-RPC payload
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvoaiSetting,
|
||||
evoai: Evoai,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
|
||||
|
||||
let processedContent = content;
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint: string = evoai.agentUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No EvoAI endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const callId = `req-${uuidv4().substring(0, 8)}`;
|
||||
const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
|
||||
|
||||
// Prepare message parts
|
||||
const parts = [
|
||||
{
|
||||
type: 'text',
|
||||
text: processedContent,
|
||||
},
|
||||
];
|
||||
|
||||
// Handle image message if present
|
||||
if (this.isImageMessage(content) && msg) {
|
||||
const media = content.split('|');
|
||||
parts[0].text = media[2] || content;
|
||||
|
||||
try {
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
let mediaBase64 = msg.message.base64 || null;
|
||||
|
||||
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||
}
|
||||
|
||||
if (mediaBase64) {
|
||||
parts.push({
|
||||
type: 'file',
|
||||
file: {
|
||||
name: msg.key.id + '.jpeg',
|
||||
mimeType: 'image/jpeg',
|
||||
bytes: mediaBase64,
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
} else {
|
||||
// Download the image
|
||||
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
|
||||
const fileContent = Buffer.from(mediaBuffer).toString('base64');
|
||||
const fileName = media[2] || `${msg.key?.id || 'image'}.jpg`;
|
||||
|
||||
parts.push({
|
||||
type: 'file',
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType: 'image/jpeg',
|
||||
bytes: fileContent,
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
} catch (fileErr) {
|
||||
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
id: callId,
|
||||
method: 'message/send',
|
||||
params: {
|
||||
contextId: session.sessionId,
|
||||
message: {
|
||||
role: 'user',
|
||||
parts,
|
||||
messageId: messageId,
|
||||
metadata: {
|
||||
messageKey: msg?.key,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.debug(`[EvoAI] Sending request to: ${endpoint}`);
|
||||
// Redact base64 file bytes from payload log
|
||||
const redactedPayload = JSON.parse(JSON.stringify(payload));
|
||||
if (redactedPayload?.params?.message?.parts) {
|
||||
redactedPayload.params.message.parts = redactedPayload.params.message.parts.map((part) => {
|
||||
if (part.type === 'file' && part.file && part.file.bytes) {
|
||||
return { ...part, file: { ...part.file, bytes: '[base64 omitted]' } };
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
this.logger.debug(`[EvoAI] Payload: ${JSON.stringify(redactedPayload)}`);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
'x-api-key': evoai.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug(`[EvoAI] Response: ${JSON.stringify(response.data)}`);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
let message = undefined;
|
||||
const result = response?.data?.result;
|
||||
|
||||
// Extract message from artifacts array
|
||||
if (result?.artifacts && Array.isArray(result.artifacts) && result.artifacts.length > 0) {
|
||||
const artifact = result.artifacts[0];
|
||||
if (artifact?.parts && Array.isArray(artifact.parts)) {
|
||||
const textPart = artifact.parts.find((p) => p.type === 'text' && p.text);
|
||||
if (textPart) message = textPart.text;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[EvoAI] Error sending message: ${error?.response?.data ? JSON.stringify(error.response.data) : error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const evoaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
agentUrl: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'agentUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'agentUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const evoaiStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const evoaiSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const evoaiIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@ -1,118 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Flowise } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { FlowiseDto } from '../dto/flowise.dto';
|
||||
import { FlowiseService } from '../services/flowise.service';
|
||||
|
||||
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
|
||||
constructor(
|
||||
private readonly flowiseService: FlowiseService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.flowise;
|
||||
this.settingsRepository = this.prismaRepository.flowiseSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('FlowiseController');
|
||||
protected readonly integrationName = 'Flowise';
|
||||
|
||||
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.flowiseIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'flowiseIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Flowise-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Override createBot to add module availability check and Flowise-specific validation
|
||||
public async createBot(instance: InstanceDto, data: FlowiseDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Flowise-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class FlowiseDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
||||
flowiseIdFallback?: string;
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
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 { flowiseController } from '@api/server.module';
|
||||
import { instanceSchema } from '@validate/instance.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { FlowiseDto, FlowiseSettingDto } from '../dto/flowise.dto';
|
||||
import {
|
||||
flowiseIgnoreJidSchema,
|
||||
flowiseSchema,
|
||||
flowiseSettingSchema,
|
||||
flowiseStatusSchema,
|
||||
} from '../validate/flowise.schema';
|
||||
|
||||
export class FlowiseRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<FlowiseDto>({
|
||||
request: req,
|
||||
schema: flowiseSchema,
|
||||
ClassRef: FlowiseDto,
|
||||
execute: (instance, data) => flowiseController.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) => flowiseController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:flowiseId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => flowiseController.fetchBot(instance, req.params.flowiseId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:flowiseId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<FlowiseDto>({
|
||||
request: req,
|
||||
schema: flowiseSchema,
|
||||
ClassRef: FlowiseDto,
|
||||
execute: (instance, data) => flowiseController.updateBot(instance, req.params.flowiseId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:flowiseId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => flowiseController.deleteBot(instance, req.params.flowiseId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<FlowiseSettingDto>({
|
||||
request: req,
|
||||
schema: flowiseSettingSchema,
|
||||
ClassRef: FlowiseSettingDto,
|
||||
execute: (instance, data) => flowiseController.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) => flowiseController.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: flowiseStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => flowiseController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:flowiseId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => flowiseController.fetchSessions(instance, req.params.flowiseId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: flowiseIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => flowiseController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
// Return the bot type for Flowise
|
||||
protected getBotType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
// Process Flowise-specific bot logic
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Implement the abstract method to send message to Flowise API
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
bot: FlowiseModel,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
question: content,
|
||||
overrideConfig: {
|
||||
sessionId: remoteJid,
|
||||
vars: {
|
||||
messageId: msg?.key?.id,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.question = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
payload.uploads = [
|
||||
{
|
||||
data: msg.message.base64 || msg.message.mediaUrl,
|
||||
type: 'url',
|
||||
name: 'Flowise.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
payload.uploads = [
|
||||
{
|
||||
data: media[1].split('?')[0],
|
||||
type: 'url',
|
||||
name: 'Flowise.png',
|
||||
mime: 'image/png',
|
||||
},
|
||||
];
|
||||
payload.question = media[2] || content;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
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 endpoint = bot.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Flowise endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
const message = response?.data?.text;
|
||||
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||
}
|
||||
}
|
||||
|
||||
// The service is now complete with just the abstract method implementations
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
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 flowiseSchema: 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' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const flowiseStatusSchema: 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 flowiseSettingSchema: 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' } },
|
||||
flowiseIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
),
|
||||
};
|
||||
|
||||
export const flowiseIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@ -5,19 +5,14 @@ import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'N8nService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,18 +51,8 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
||||
apiKey: instance.token,
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.chatInput = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
// Audio transcription removed due to OpenAI integration removal
|
||||
// Audio messages will be sent as-is without transcription
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (n8n.basicAuthUser && n8n.basicAuthPass) {
|
||||
|
||||
@ -1,482 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { OpenaiCredsDto, OpenaiDto } from '@api/integrations/chatbot/openai/dto/openai.dto';
|
||||
import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Openai } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { IntegrationSession, OpenaiBot } from '@prisma/client';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class OpenaiController extends BaseChatbotController<OpenaiBot, OpenaiDto> {
|
||||
constructor(
|
||||
private readonly openaiService: OpenaiService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.openaiBot;
|
||||
this.settingsRepository = this.prismaRepository.openaiSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
this.credsRepository = this.prismaRepository.openaiCreds;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('OpenaiController');
|
||||
protected readonly integrationName = 'Openai';
|
||||
|
||||
integrationEnabled = configService.get<Openai>('OPENAI').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
private client: OpenAI;
|
||||
private credsRepository: any;
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.openaiIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'openaiIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: OpenaiDto): Record<string, any> {
|
||||
return {
|
||||
openaiCredsId: data.openaiCredsId,
|
||||
botType: data.botType,
|
||||
assistantId: data.assistantId,
|
||||
functionUrl: data.functionUrl,
|
||||
model: data.model,
|
||||
systemMessages: data.systemMessages,
|
||||
assistantMessages: data.assistantMessages,
|
||||
userMessages: data.userMessages,
|
||||
maxTokens: data.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: OpenaiDto): Record<string, any> {
|
||||
return {
|
||||
openaiCredsId: data.openaiCredsId,
|
||||
botType: data.botType,
|
||||
assistantId: data.assistantId,
|
||||
functionUrl: data.functionUrl,
|
||||
model: data.model,
|
||||
systemMessages: data.systemMessages,
|
||||
assistantMessages: data.assistantMessages,
|
||||
userMessages: data.userMessages,
|
||||
maxTokens: data.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: OpenaiDto): Promise<void> {
|
||||
let whereDuplication: any = {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
};
|
||||
|
||||
if (data.botType === 'assistant') {
|
||||
if (!data.assistantId) throw new Error('Assistant ID is required');
|
||||
|
||||
whereDuplication = {
|
||||
...whereDuplication,
|
||||
assistantId: data.assistantId,
|
||||
botType: data.botType,
|
||||
};
|
||||
} else if (data.botType === 'chatCompletion') {
|
||||
if (!data.model) throw new Error('Model is required');
|
||||
if (!data.maxTokens) throw new Error('Max tokens is required');
|
||||
|
||||
whereDuplication = {
|
||||
...whereDuplication,
|
||||
model: data.model,
|
||||
maxTokens: data.maxTokens,
|
||||
botType: data.botType,
|
||||
};
|
||||
} else {
|
||||
throw new Error('Bot type is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: whereDuplication,
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('OpenAI Bot already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to handle OpenAI-specific credential logic
|
||||
public async createBot(instance: InstanceDto, data: OpenaiDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// OpenAI specific validation
|
||||
let whereDuplication: any = {
|
||||
instanceId: instanceId,
|
||||
};
|
||||
|
||||
if (data.botType === 'assistant') {
|
||||
if (!data.assistantId) throw new Error('Assistant ID is required');
|
||||
|
||||
whereDuplication = {
|
||||
...whereDuplication,
|
||||
assistantId: data.assistantId,
|
||||
botType: data.botType,
|
||||
};
|
||||
} else if (data.botType === 'chatCompletion') {
|
||||
if (!data.model) throw new Error('Model is required');
|
||||
if (!data.maxTokens) throw new Error('Max tokens is required');
|
||||
|
||||
whereDuplication = {
|
||||
...whereDuplication,
|
||||
model: data.model,
|
||||
maxTokens: data.maxTokens,
|
||||
botType: data.botType,
|
||||
};
|
||||
} else {
|
||||
throw new Error('Bot type is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: whereDuplication,
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Openai Bot already exists');
|
||||
}
|
||||
|
||||
// Check if settings exist and create them if not
|
||||
const existingSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingSettings) {
|
||||
// Create default settings with the OpenAI credentials
|
||||
await this.settings(instance, {
|
||||
openaiCredsId: data.openaiCredsId,
|
||||
expire: data.expire || 300,
|
||||
keywordFinish: data.keywordFinish || 'bye',
|
||||
delayMessage: data.delayMessage || 1000,
|
||||
unknownMessage: data.unknownMessage || 'Sorry, I dont understand',
|
||||
listeningFromMe: data.listeningFromMe !== undefined ? data.listeningFromMe : true,
|
||||
stopBotFromMe: data.stopBotFromMe !== undefined ? data.stopBotFromMe : true,
|
||||
keepOpen: data.keepOpen !== undefined ? data.keepOpen : false,
|
||||
debounceTime: data.debounceTime || 1,
|
||||
ignoreJids: data.ignoreJids || [],
|
||||
speechToText: false,
|
||||
});
|
||||
} else if (!existingSettings.openaiCredsId && data.openaiCredsId) {
|
||||
// Update settings with OpenAI credentials if they're missing
|
||||
await this.settingsRepository.update({
|
||||
where: {
|
||||
id: existingSettings.id,
|
||||
},
|
||||
data: {
|
||||
OpenaiCreds: {
|
||||
connect: {
|
||||
id: data.openaiCredsId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Let the base class handle the rest of the bot creation process
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process OpenAI-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: OpenaiBot,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.openaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Credentials - OpenAI specific functionality
|
||||
public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (!data.apiKey) throw new BadRequestException('API Key is required');
|
||||
if (!data.name) throw new BadRequestException('Name is required');
|
||||
|
||||
// Check if API key already exists
|
||||
const existingApiKey = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingApiKey) {
|
||||
throw new BadRequestException('This API key is already registered. Please use a different API key.');
|
||||
}
|
||||
|
||||
// Check if name already exists for this instance
|
||||
const existingName = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
name: data.name,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingName) {
|
||||
throw new BadRequestException('This credential name is already in use. Please choose a different name.');
|
||||
}
|
||||
|
||||
try {
|
||||
const creds = await this.credsRepository.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
apiKey: data.apiKey,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return creds;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating openai creds');
|
||||
}
|
||||
}
|
||||
|
||||
public async findOpenaiCreds(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const creds = await this.credsRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiAssistant: true,
|
||||
},
|
||||
});
|
||||
|
||||
return creds;
|
||||
}
|
||||
|
||||
public async deleteCreds(instance: InstanceDto, openaiCredsId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const creds = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
id: openaiCredsId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
throw new Error('Openai Creds not found');
|
||||
}
|
||||
|
||||
if (creds.instanceId !== instanceId) {
|
||||
throw new Error('Openai Creds not found');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.credsRepository.delete({
|
||||
where: {
|
||||
id: openaiCredsId,
|
||||
},
|
||||
});
|
||||
|
||||
return { openaiCreds: { id: openaiCredsId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting openai creds');
|
||||
}
|
||||
}
|
||||
|
||||
// Override the settings method to handle the OpenAI credentials
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const existingSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Convert keywordFinish to string if it's an array
|
||||
const keywordFinish = data.keywordFinish;
|
||||
|
||||
// Additional OpenAI-specific fields
|
||||
const settingsData = {
|
||||
expire: data.expire,
|
||||
keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
openaiIdFallback: data.fallbackId,
|
||||
OpenaiCreds: data.openaiCredsId
|
||||
? {
|
||||
connect: {
|
||||
id: data.openaiCredsId,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
speechToText: data.speechToText,
|
||||
};
|
||||
|
||||
if (existingSettings) {
|
||||
const settings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: existingSettings.id,
|
||||
},
|
||||
data: settingsData,
|
||||
});
|
||||
|
||||
// Map the specific fallback field to a generic 'fallbackId' in the response
|
||||
return {
|
||||
...settings,
|
||||
fallbackId: settings.openaiIdFallback,
|
||||
};
|
||||
} else {
|
||||
const settings = await this.settingsRepository.create({
|
||||
data: {
|
||||
...settingsData,
|
||||
Instance: {
|
||||
connect: {
|
||||
id: instanceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Map the specific fallback field to a generic 'fallbackId' in the response
|
||||
return {
|
||||
...settings,
|
||||
fallbackId: settings.openaiIdFallback,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Models - OpenAI specific functionality
|
||||
public async getModels(instance: InstanceDto, openaiCredsId?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (!instanceId) throw new Error('Instance not found');
|
||||
|
||||
let apiKey: string;
|
||||
|
||||
if (openaiCredsId) {
|
||||
// Use specific credential ID if provided
|
||||
const creds = await this.credsRepository.findFirst({
|
||||
where: {
|
||||
id: openaiCredsId,
|
||||
instanceId: instanceId, // Ensure the credential belongs to this instance
|
||||
},
|
||||
});
|
||||
|
||||
if (!creds) throw new Error('OpenAI credentials not found for the provided ID');
|
||||
|
||||
apiKey = creds.apiKey;
|
||||
} else {
|
||||
// Use default credentials from settings if no ID provided
|
||||
const defaultSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!defaultSettings) throw new Error('Settings not found');
|
||||
|
||||
if (!defaultSettings.OpenaiCreds)
|
||||
throw new Error(
|
||||
'OpenAI credentials not found. Please create credentials and associate them with the settings.',
|
||||
);
|
||||
|
||||
apiKey = defaultSettings.OpenaiCreds.apiKey;
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({ apiKey });
|
||||
|
||||
const models: any = await this.client.models.list();
|
||||
|
||||
return models?.body?.data;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching models');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class OpenaiDto extends BaseChatbotDto {
|
||||
openaiCredsId: string;
|
||||
botType: string;
|
||||
assistantId?: string;
|
||||
functionUrl?: string;
|
||||
model?: string;
|
||||
systemMessages?: string[];
|
||||
assistantMessages?: string[];
|
||||
userMessages?: string[];
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export class OpenaiSettingDto extends BaseChatbotSettingDto {
|
||||
openaiCredsId?: string;
|
||||
openaiIdFallback?: string;
|
||||
speechToText?: boolean;
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { OpenaiCredsDto, OpenaiDto, OpenaiSettingDto } from '@api/integrations/chatbot/openai/dto/openai.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { openaiController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
openaiCredsSchema,
|
||||
openaiIgnoreJidSchema,
|
||||
openaiSchema,
|
||||
openaiSettingSchema,
|
||||
openaiStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class OpenaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('creds'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiCredsDto>({
|
||||
request: req,
|
||||
schema: openaiCredsSchema,
|
||||
ClassRef: OpenaiCredsDto,
|
||||
execute: (instance, data) => openaiController.createOpenaiCreds(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('creds'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.findOpenaiCreds(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('creds/:openaiCredsId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.deleteCreds(instance, req.params.openaiCredsId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiDto>({
|
||||
request: req,
|
||||
schema: openaiSchema,
|
||||
ClassRef: OpenaiDto,
|
||||
execute: (instance, data) => openaiController.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) => openaiController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.fetchBot(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiDto>({
|
||||
request: req,
|
||||
schema: openaiSchema,
|
||||
ClassRef: OpenaiDto,
|
||||
execute: (instance, data) => openaiController.updateBot(instance, req.params.openaiBotId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.deleteBot(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiSettingDto>({
|
||||
request: req,
|
||||
schema: openaiSettingSchema,
|
||||
ClassRef: OpenaiSettingDto,
|
||||
execute: (instance, data) => openaiController.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) => openaiController.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: openaiStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => openaiController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.fetchSessions(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: openaiIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => openaiController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('getModels'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
@ -1,734 +0,0 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, Language, Openai as OpenaiConfig } from '@config/env.config';
|
||||
import { IntegrationSession, OpenaiBot, OpenaiSetting } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { downloadMediaMessage } from 'baileys';
|
||||
import { isURL } from 'class-validator';
|
||||
import FormData from 'form-data';
|
||||
import OpenAI from 'openai';
|
||||
import P from 'pino';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
|
||||
/**
|
||||
* OpenAI service that extends the common BaseChatbotService
|
||||
* Handles both Assistant API and ChatCompletion API
|
||||
*/
|
||||
export class OpenaiService extends BaseChatbotService<OpenaiBot, OpenaiSetting> {
|
||||
protected client: OpenAI;
|
||||
|
||||
constructor(waMonitor: WAMonitoringService, prismaRepository: PrismaRepository, configService: ConfigService) {
|
||||
super(waMonitor, prismaRepository, 'OpenaiService', configService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for OpenAI
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the OpenAI client with the provided API key
|
||||
*/
|
||||
protected initClient(apiKey: string) {
|
||||
this.client = new OpenAI({ apiKey });
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a message based on the bot type (assistant or chat completion)
|
||||
*/
|
||||
public async process(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
openaiBot: OpenaiBot,
|
||||
session: IntegrationSession,
|
||||
settings: OpenaiSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.log(`Starting process for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
|
||||
|
||||
// Handle audio message transcription
|
||||
if (content.startsWith('audioMessage|') && msg) {
|
||||
this.logger.log('Detected audio message, attempting to transcribe');
|
||||
|
||||
// Get OpenAI credentials for transcription
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: openaiBot.openaiCredsId },
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found. CredsId: ${openaiBot.openaiCredsId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize OpenAI client for transcription
|
||||
this.initClient(creds.apiKey);
|
||||
|
||||
// Transcribe the audio
|
||||
const transcription = await this.speechToText(msg, instance);
|
||||
|
||||
if (transcription) {
|
||||
this.logger.log(`Audio transcribed: ${transcription}`);
|
||||
// Replace the audio message identifier with the transcription
|
||||
content = transcription;
|
||||
} else {
|
||||
this.logger.error('Failed to transcribe audio');
|
||||
await this.sendMessageWhatsApp(
|
||||
instance,
|
||||
remoteJid,
|
||||
"Sorry, I couldn't transcribe your audio message. Could you please type your message instead?",
|
||||
settings,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Get the OpenAI credentials
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: openaiBot.openaiCredsId },
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found. CredsId: ${openaiBot.openaiCredsId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize OpenAI client
|
||||
this.initClient(creds.apiKey);
|
||||
}
|
||||
|
||||
// Handle keyword finish
|
||||
const keywordFinish = settings?.keywordFinish || '';
|
||||
const normalizedContent = content.toLowerCase().trim();
|
||||
if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
|
||||
if (settings?.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.delete({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await sendTelemetry('/openai/session/finish');
|
||||
return;
|
||||
}
|
||||
|
||||
// If session is new or doesn't exist
|
||||
if (!session) {
|
||||
const data = {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: openaiBot.id,
|
||||
};
|
||||
|
||||
const createSession = await this.createNewSession(
|
||||
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
|
||||
data,
|
||||
this.getBotType(),
|
||||
);
|
||||
|
||||
await this.initNewSession(
|
||||
instance,
|
||||
remoteJid,
|
||||
openaiBot,
|
||||
settings,
|
||||
createSession.session,
|
||||
content,
|
||||
pushName,
|
||||
msg,
|
||||
);
|
||||
|
||||
await sendTelemetry('/openai/session/start');
|
||||
return;
|
||||
}
|
||||
|
||||
// If session exists but is paused
|
||||
if (session.status === 'paused') {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Process with the appropriate API based on bot type
|
||||
await this.sendMessageToBot(instance, session, settings, openaiBot, remoteJid, pushName || '', content, msg);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in process: ${error.message || JSON.stringify(error)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to OpenAI - this handles both Assistant API and ChatCompletion API
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: OpenaiSetting,
|
||||
openaiBot: OpenaiBot,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
this.logger.log(`Sending message to bot for remoteJid: ${remoteJid}, bot type: ${openaiBot.botType}`);
|
||||
|
||||
if (!this.client) {
|
||||
this.logger.log('Client not initialized, initializing now');
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: openaiBot.openaiCredsId },
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found in sendMessageToBot. CredsId: ${openaiBot.openaiCredsId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initClient(creds.apiKey);
|
||||
}
|
||||
|
||||
try {
|
||||
let message: string;
|
||||
|
||||
// Handle different bot types
|
||||
if (openaiBot.botType === 'assistant') {
|
||||
this.logger.log('Processing with Assistant API');
|
||||
message = await this.processAssistantMessage(
|
||||
instance,
|
||||
session,
|
||||
openaiBot,
|
||||
remoteJid,
|
||||
pushName,
|
||||
false, // Not fromMe
|
||||
content,
|
||||
msg,
|
||||
);
|
||||
} else {
|
||||
this.logger.log('Processing with ChatCompletion API');
|
||||
message = await this.processChatCompletionMessage(instance, openaiBot, remoteJid, content, msg);
|
||||
}
|
||||
|
||||
this.logger.log(`Got response from OpenAI: ${message?.substring(0, 50)}${message?.length > 50 ? '...' : ''}`);
|
||||
|
||||
// Send the response
|
||||
if (message) {
|
||||
this.logger.log('Sending message to WhatsApp');
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings, true);
|
||||
} else {
|
||||
this.logger.error('No message to send to WhatsApp');
|
||||
}
|
||||
|
||||
// Update session status
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
|
||||
if (error.response) {
|
||||
this.logger.error(`API Response data: ${JSON.stringify(error.response.data || {})}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process message using the OpenAI Assistant API
|
||||
*/
|
||||
private async processAssistantMessage(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
openaiBot: OpenaiBot,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
fromMe: boolean,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<string> {
|
||||
const messageData: any = {
|
||||
role: fromMe ? 'assistant' : 'user',
|
||||
content: [{ type: 'text', text: content }],
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
let mediaBase64 = msg.message.base64 || null;
|
||||
|
||||
if (msg.message.mediaUrl && isURL(msg.message.mediaUrl)) {
|
||||
const result = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' });
|
||||
mediaBase64 = Buffer.from(result.data).toString('base64');
|
||||
}
|
||||
|
||||
if (mediaBase64) {
|
||||
messageData.content = [
|
||||
{ type: 'text', text: media[2] || content },
|
||||
{ type: 'image_url', image_url: { url: mediaBase64 } },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
const url = media[1].split('?')[0];
|
||||
|
||||
messageData.content = [
|
||||
{ type: 'text', text: media[2] || content },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get thread ID from session or create new thread
|
||||
let threadId = session.sessionId;
|
||||
|
||||
// Create a new thread if one doesn't exist or invalid format
|
||||
if (!threadId || threadId === remoteJid) {
|
||||
const newThread = await this.client.beta.threads.create();
|
||||
threadId = newThread.id;
|
||||
|
||||
// Save the new thread ID to the session
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
sessionId: threadId,
|
||||
},
|
||||
});
|
||||
this.logger.log(`Created new thread ID: ${threadId} for session: ${session.id}`);
|
||||
}
|
||||
|
||||
// Add message to thread
|
||||
await this.client.beta.threads.messages.create(threadId, messageData);
|
||||
|
||||
if (fromMe) {
|
||||
sendTelemetry('/message/sendText');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Run the assistant
|
||||
const runAssistant = await this.client.beta.threads.runs.create(threadId, {
|
||||
assistant_id: openaiBot.assistantId,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
// Wait for the assistant to complete
|
||||
const response = await this.getAIResponse(threadId, runAssistant.id, openaiBot.functionUrl, remoteJid, pushName);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
// Extract the response text safely with type checking
|
||||
let responseText = "I couldn't generate a proper response. Please try again.";
|
||||
try {
|
||||
const messages = response?.data || [];
|
||||
if (messages.length > 0) {
|
||||
const messageContent = messages[0]?.content || [];
|
||||
if (messageContent.length > 0) {
|
||||
const textContent = messageContent[0];
|
||||
if (textContent && 'text' in textContent && textContent.text && 'value' in textContent.text) {
|
||||
responseText = textContent.text.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error extracting response text: ${error}`);
|
||||
}
|
||||
|
||||
// Update session with the thread ID to ensure continuity
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: threadId, // Ensure thread ID is saved consistently
|
||||
},
|
||||
});
|
||||
|
||||
// Return fallback message if unable to extract text
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process message using the OpenAI ChatCompletion API
|
||||
*/
|
||||
private async processChatCompletionMessage(
|
||||
instance: any,
|
||||
openaiBot: OpenaiBot,
|
||||
remoteJid: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<string> {
|
||||
this.logger.log('Starting processChatCompletionMessage');
|
||||
|
||||
// Check if client is initialized
|
||||
if (!this.client) {
|
||||
this.logger.log('Client not initialized in processChatCompletionMessage, initializing now');
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: openaiBot.openaiCredsId },
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found. CredsId: ${openaiBot.openaiCredsId}`);
|
||||
return 'Error: OpenAI credentials not found';
|
||||
}
|
||||
|
||||
this.initClient(creds.apiKey);
|
||||
}
|
||||
|
||||
// Check if model is defined
|
||||
if (!openaiBot.model) {
|
||||
this.logger.error('OpenAI model not defined');
|
||||
return 'Error: OpenAI model not configured';
|
||||
}
|
||||
|
||||
this.logger.log(`Using model: ${openaiBot.model}, max tokens: ${openaiBot.maxTokens || 500}`);
|
||||
|
||||
// Get existing conversation history from the session
|
||||
const session = await this.prismaRepository.integrationSession.findFirst({
|
||||
where: {
|
||||
remoteJid,
|
||||
botId: openaiBot.id,
|
||||
status: 'opened',
|
||||
},
|
||||
});
|
||||
|
||||
let conversationHistory = [];
|
||||
|
||||
if (session && session.context) {
|
||||
try {
|
||||
const sessionData =
|
||||
typeof session.context === 'string' ? JSON.parse(session.context as string) : session.context;
|
||||
|
||||
conversationHistory = sessionData.history || [];
|
||||
this.logger.log(`Retrieved conversation history from session, ${conversationHistory.length} messages`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error parsing session context: ${error.message}`);
|
||||
// Continue with empty history if we can't parse the session data
|
||||
conversationHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Log bot data
|
||||
this.logger.log(`Bot data - systemMessages: ${JSON.stringify(openaiBot.systemMessages || [])}`);
|
||||
this.logger.log(`Bot data - assistantMessages: ${JSON.stringify(openaiBot.assistantMessages || [])}`);
|
||||
this.logger.log(`Bot data - userMessages: ${JSON.stringify(openaiBot.userMessages || [])}`);
|
||||
|
||||
// Prepare system messages
|
||||
const systemMessages: any = openaiBot.systemMessages || [];
|
||||
const messagesSystem: any[] = systemMessages.map((message) => {
|
||||
return {
|
||||
role: 'system',
|
||||
content: message,
|
||||
};
|
||||
});
|
||||
|
||||
// Prepare assistant messages
|
||||
const assistantMessages: any = openaiBot.assistantMessages || [];
|
||||
const messagesAssistant: any[] = assistantMessages.map((message) => {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: message,
|
||||
};
|
||||
});
|
||||
|
||||
// Prepare user messages
|
||||
const userMessages: any = openaiBot.userMessages || [];
|
||||
const messagesUser: any[] = userMessages.map((message) => {
|
||||
return {
|
||||
role: 'user',
|
||||
content: message,
|
||||
};
|
||||
});
|
||||
|
||||
// Prepare current message
|
||||
const messageData: any = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: content }],
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
this.logger.log('Found image message');
|
||||
const media = content.split('|');
|
||||
|
||||
if (msg.message.mediaUrl || msg.message.base64) {
|
||||
messageData.content = [
|
||||
{ type: 'text', text: media[2] || content },
|
||||
{ type: 'image_url', image_url: { url: msg.message.base64 || msg.message.mediaUrl } },
|
||||
];
|
||||
} else {
|
||||
const url = media[1].split('?')[0];
|
||||
|
||||
messageData.content = [
|
||||
{ type: 'text', text: media[2] || content },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all messages: system messages, pre-defined messages, conversation history, and current message
|
||||
const messages: any[] = [
|
||||
...messagesSystem,
|
||||
...messagesAssistant,
|
||||
...messagesUser,
|
||||
...conversationHistory,
|
||||
messageData,
|
||||
];
|
||||
|
||||
this.logger.log(`Final messages payload: ${JSON.stringify(messages)}`);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
this.logger.log('Setting typing indicator');
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
// Send the request to OpenAI
|
||||
try {
|
||||
this.logger.log('Sending request to OpenAI API');
|
||||
const completions = await this.client.chat.completions.create({
|
||||
model: openaiBot.model,
|
||||
messages: messages,
|
||||
max_tokens: openaiBot.maxTokens || 500, // Add default if maxTokens is missing
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
const responseContent = completions.choices[0].message.content;
|
||||
this.logger.log(`Received response from OpenAI: ${JSON.stringify(completions.choices[0])}`);
|
||||
|
||||
// Add the current exchange to the conversation history and update the session
|
||||
conversationHistory.push(messageData);
|
||||
conversationHistory.push({
|
||||
role: 'assistant',
|
||||
content: responseContent,
|
||||
});
|
||||
|
||||
// Limit history length to avoid token limits (keep last 10 messages)
|
||||
if (conversationHistory.length > 10) {
|
||||
conversationHistory = conversationHistory.slice(conversationHistory.length - 10);
|
||||
}
|
||||
|
||||
// Save the updated conversation history to the session
|
||||
if (session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: { id: session.id },
|
||||
data: {
|
||||
context: JSON.stringify({
|
||||
history: conversationHistory,
|
||||
}),
|
||||
},
|
||||
});
|
||||
this.logger.log(`Updated session with conversation history, now ${conversationHistory.length} messages`);
|
||||
}
|
||||
|
||||
return responseContent;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error calling OpenAI: ${error.message || JSON.stringify(error)}`);
|
||||
if (error.response) {
|
||||
this.logger.error(`API Response status: ${error.response.status}`);
|
||||
this.logger.error(`API Response data: ${JSON.stringify(error.response.data || {})}`);
|
||||
}
|
||||
return `Sorry, there was an error: ${error.message || 'Unknown error'}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for and retrieve the AI response
|
||||
*/
|
||||
private async getAIResponse(
|
||||
threadId: string,
|
||||
runId: string,
|
||||
functionUrl: string | null,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
) {
|
||||
let status = await this.client.beta.threads.runs.retrieve(threadId, runId);
|
||||
|
||||
let maxRetries = 60; // 1 minute with 1s intervals
|
||||
const checkInterval = 1000; // 1 second
|
||||
|
||||
while (
|
||||
status.status !== 'completed' &&
|
||||
status.status !== 'failed' &&
|
||||
status.status !== 'cancelled' &&
|
||||
status.status !== 'expired' &&
|
||||
maxRetries > 0
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
status = await this.client.beta.threads.runs.retrieve(threadId, runId);
|
||||
|
||||
// Handle tool calls
|
||||
if (status.status === 'requires_action' && status.required_action?.type === 'submit_tool_outputs') {
|
||||
const toolCalls = status.required_action.submit_tool_outputs.tool_calls;
|
||||
const toolOutputs = [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (functionUrl) {
|
||||
try {
|
||||
const payloadData = JSON.parse(toolCall.function.arguments);
|
||||
|
||||
// Add context
|
||||
payloadData.remoteJid = remoteJid;
|
||||
payloadData.pushName = pushName;
|
||||
|
||||
const response = await axios.post(functionUrl, {
|
||||
functionName: toolCall.function.name,
|
||||
functionArguments: payloadData,
|
||||
});
|
||||
|
||||
toolOutputs.push({
|
||||
tool_call_id: toolCall.id,
|
||||
output: JSON.stringify(response.data),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error calling function: ${error}`);
|
||||
toolOutputs.push({
|
||||
tool_call_id: toolCall.id,
|
||||
output: JSON.stringify({ error: 'Function call failed' }),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toolOutputs.push({
|
||||
tool_call_id: toolCall.id,
|
||||
output: JSON.stringify({ error: 'No function URL configured' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.client.beta.threads.runs.submitToolOutputs(threadId, runId, {
|
||||
tool_outputs: toolOutputs,
|
||||
});
|
||||
}
|
||||
|
||||
maxRetries--;
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
const messages = await this.client.beta.threads.messages.list(threadId);
|
||||
return messages;
|
||||
} else {
|
||||
this.logger.error(`Assistant run failed with status: ${status.status}`);
|
||||
return { data: [{ content: [{ text: { value: 'Failed to get a response from the assistant.' } }] }] };
|
||||
}
|
||||
}
|
||||
|
||||
protected isImageMessage(content: string): boolean {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of speech-to-text transcription for audio messages
|
||||
*/
|
||||
public async speechToText(msg: any, instance: any): Promise<string | null> {
|
||||
const settings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
this.logger.error(`OpenAI settings not found. InstanceId: ${instance.instanceId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const creds = await this.prismaRepository.openaiCreds.findUnique({
|
||||
where: { id: settings.openaiCredsId },
|
||||
});
|
||||
|
||||
if (!creds) {
|
||||
this.logger.error(`OpenAI credentials not found. CredsId: ${settings.openaiCredsId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let audio: Buffer;
|
||||
|
||||
if (msg.message.mediaUrl) {
|
||||
audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => {
|
||||
return Buffer.from(response.data, 'binary');
|
||||
});
|
||||
} else if (msg.message.base64) {
|
||||
audio = Buffer.from(msg.message.base64, 'base64');
|
||||
} else {
|
||||
// Fallback for raw WhatsApp audio messages that need downloadMediaMessage
|
||||
audio = await downloadMediaMessage(
|
||||
{ key: msg.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: instance,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
|
||||
? 'pt'
|
||||
: this.configService.get<Language>('LANGUAGE');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', audio, 'audio.ogg');
|
||||
formData.append('model', 'whisper-1');
|
||||
formData.append('language', lang);
|
||||
|
||||
const apiKey = creds?.apiKey || this.configService.get<OpenaiConfig>('OPENAI').API_KEY_GLOBAL;
|
||||
|
||||
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response?.data?.text;
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
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 openaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
openaiCredsId: { type: 'string' },
|
||||
botType: { type: 'string', enum: ['assistant', 'chatCompletion'] },
|
||||
assistantId: { type: 'string' },
|
||||
functionUrl: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
systemMessages: { type: 'array', items: { type: 'string' } },
|
||||
assistantMessages: { type: 'array', items: { type: 'string' } },
|
||||
userMessages: { type: 'array', items: { type: 'string' } },
|
||||
maxTokens: { type: 'integer' },
|
||||
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', 'openaiCredsId', 'botType', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'openaiCredsId', 'botType', 'triggerType'),
|
||||
};
|
||||
|
||||
export const openaiCredsSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
},
|
||||
required: ['name', 'apiKey'],
|
||||
...isNotEmpty('name', 'apiKey'),
|
||||
};
|
||||
|
||||
export const openaiStatusSchema: 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 openaiSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
openaiCredsId: { 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' },
|
||||
speechToText: { type: 'boolean' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
openaiIdFallback: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'openaiCredsId',
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'openaiCredsId',
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
),
|
||||
};
|
||||
|
||||
export const openaiIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@ -1,318 +0,0 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Events } from '@api/types/wa.types';
|
||||
import { configService, Typebot } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class TypebotController extends BaseChatbotController<TypebotModel, TypebotDto> {
|
||||
constructor(
|
||||
private readonly typebotService: TypebotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.typebot;
|
||||
this.settingsRepository = this.prismaRepository.typebotSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('TypebotController');
|
||||
protected readonly integrationName = 'Typebot';
|
||||
|
||||
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.typebotIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'typebotIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'typebot';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Typebot-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: TypebotModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Map to the original processTypebot method signature
|
||||
await this.typebotService.processTypebot(
|
||||
instance,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
bot,
|
||||
bot.url,
|
||||
settings.expire,
|
||||
bot.typebot,
|
||||
settings.keywordFinish,
|
||||
settings.delayMessage,
|
||||
settings.unknownMessage,
|
||||
settings.listeningFromMe,
|
||||
settings.stopBotFromMe,
|
||||
settings.keepOpen,
|
||||
content,
|
||||
{}, // prefilledVariables (optional)
|
||||
);
|
||||
}
|
||||
|
||||
// TypeBot specific method for starting a bot from API
|
||||
public async startBot(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
if (data.remoteJid === 'status@broadcast') return;
|
||||
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!instanceData) throw new Error('Instance not found');
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const url = data.url;
|
||||
const typebot = data.typebot;
|
||||
const startSession = data.startSession;
|
||||
const variables = data.variables;
|
||||
let expire = data?.typebot?.expire;
|
||||
let keywordFinish = data?.typebot?.keywordFinish;
|
||||
let delayMessage = data?.typebot?.delayMessage;
|
||||
let unknownMessage = data?.typebot?.unknownMessage;
|
||||
let listeningFromMe = data?.typebot?.listeningFromMe;
|
||||
let stopBotFromMe = data?.typebot?.stopBotFromMe;
|
||||
let keepOpen = data?.typebot?.keepOpen;
|
||||
let debounceTime = data?.typebot?.debounceTime;
|
||||
let ignoreJids = data?.typebot?.ignoreJids;
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceData.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(defaultSettingCheck?.ignoreJids, remoteJid)) throw new Error('Jid not allowed');
|
||||
|
||||
if (
|
||||
!expire ||
|
||||
!keywordFinish ||
|
||||
!delayMessage ||
|
||||
!unknownMessage ||
|
||||
!listeningFromMe ||
|
||||
!stopBotFromMe ||
|
||||
!keepOpen ||
|
||||
!debounceTime ||
|
||||
!ignoreJids
|
||||
) {
|
||||
if (expire === undefined || expire === null) expire = defaultSettingCheck.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null)
|
||||
listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: expire,
|
||||
keywordFinish: keywordFinish,
|
||||
delayMessage: delayMessage,
|
||||
unknownMessage: unknownMessage,
|
||||
listeningFromMe: listeningFromMe,
|
||||
stopBotFromMe: stopBotFromMe,
|
||||
keepOpen: keepOpen,
|
||||
debounceTime: debounceTime,
|
||||
ignoreJids: ignoreJids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const prefilledVariables: any = {};
|
||||
|
||||
if (variables?.length) {
|
||||
variables.forEach((variable: { name: string | number; value: string }) => {
|
||||
prefilledVariables[variable.name] = variable.value;
|
||||
});
|
||||
}
|
||||
|
||||
if (startSession) {
|
||||
let findBot: any = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
instanceId: instanceData.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!findBot) {
|
||||
findBot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: true,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
instanceId: instanceData.id,
|
||||
expire: expire,
|
||||
keywordFinish: keywordFinish,
|
||||
delayMessage: delayMessage,
|
||||
unknownMessage: unknownMessage,
|
||||
listeningFromMe: listeningFromMe,
|
||||
stopBotFromMe: stopBotFromMe,
|
||||
keepOpen: keepOpen,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
instanceId: instanceData.id,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
// Use the original processTypebot method with all parameters
|
||||
await this.typebotService.processTypebot(
|
||||
this.waMonitor.waInstances[instanceData.name],
|
||||
remoteJid,
|
||||
null, // msg
|
||||
null, // session
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
'init',
|
||||
prefilledVariables,
|
||||
);
|
||||
} else {
|
||||
const id = Math.floor(Math.random() * 10000000000).toString();
|
||||
|
||||
try {
|
||||
const version = configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let url: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`;
|
||||
|
||||
reqData = {
|
||||
prefilledVariables: prefilledVariables,
|
||||
};
|
||||
} else {
|
||||
url = `${data.url}/api/v1/sendMessage`;
|
||||
|
||||
reqData = {
|
||||
startParams: {
|
||||
publicId: data.typebot,
|
||||
prefilledVariables: prefilledVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const request = await axios.post(url, reqData);
|
||||
|
||||
await this.typebotService.sendWAMessage(
|
||||
instanceData,
|
||||
null,
|
||||
{
|
||||
expire: expire,
|
||||
keywordFinish: keywordFinish,
|
||||
delayMessage: delayMessage,
|
||||
unknownMessage: unknownMessage,
|
||||
listeningFromMe: listeningFromMe,
|
||||
stopBotFromMe: stopBotFromMe,
|
||||
keepOpen: keepOpen,
|
||||
},
|
||||
remoteJid,
|
||||
request.data.messages,
|
||||
request.data.input,
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
variables: variables,
|
||||
sessionId: id,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
typebot: {
|
||||
...instance,
|
||||
typebot: {
|
||||
url: url,
|
||||
remoteJid: remoteJid,
|
||||
typebot: typebot,
|
||||
prefilledVariables: prefilledVariables,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class PrefilledVariables {
|
||||
remoteJid?: string;
|
||||
pushName?: string;
|
||||
messageType?: string;
|
||||
additionalData?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class TypebotDto extends BaseChatbotDto {
|
||||
url: string;
|
||||
typebot: string;
|
||||
}
|
||||
|
||||
export class TypebotSettingDto extends BaseChatbotSettingDto {
|
||||
typebotIdFallback?: string;
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { typebotController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
typebotIgnoreJidSchema,
|
||||
typebotSchema,
|
||||
typebotSettingSchema,
|
||||
typebotStartSchema,
|
||||
typebotStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class TypebotRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotDto>({
|
||||
request: req,
|
||||
schema: typebotSchema,
|
||||
ClassRef: TypebotDto,
|
||||
execute: (instance, data) => typebotController.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) => typebotController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.fetchBot(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotDto>({
|
||||
request: req,
|
||||
schema: typebotSchema,
|
||||
ClassRef: TypebotDto,
|
||||
execute: (instance, data) => typebotController.updateBot(instance, req.params.typebotId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.deleteBot(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotSettingDto>({
|
||||
request: req,
|
||||
schema: typebotSettingSchema,
|
||||
ClassRef: TypebotSettingDto,
|
||||
execute: (instance, data) => typebotController.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) => typebotController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('start'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStartSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.startBot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.fetchSessions(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: typebotIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => typebotController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,97 +0,0 @@
|
||||
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 typebotSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
typebot: { 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' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'url', 'typebot', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'url', 'typebot', 'triggerType'),
|
||||
};
|
||||
|
||||
export const typebotStatusSchema: 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 typebotStartSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
typebot: { type: 'string' },
|
||||
},
|
||||
required: ['remoteJid', 'url', 'typebot'],
|
||||
...isNotEmpty('remoteJid', 'url', 'typebot'),
|
||||
};
|
||||
|
||||
export const typebotSettingSchema: 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' },
|
||||
typebotIdFallback: { type: 'string' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'],
|
||||
...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'),
|
||||
};
|
||||
|
||||
export const typebotIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import { ChatwootInstanceMixin } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { EventInstanceMixin } from '@api/integrations/event/event.dto';
|
||||
|
||||
export type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
export class IntegrationDto extends EventInstanceMixin(ChatwootInstanceMixin(class {})) {}
|
||||
export class IntegrationDto extends EventInstanceMixin(class {}) {}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CacheEngine } from '@cache/cacheengine';
|
||||
import { Chatwoot, configService, ProviderSession } from '@config/env.config';
|
||||
import { configService, ProviderSession } from '@config/env.config';
|
||||
import { eventEmitter } from '@config/event.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
@ -17,20 +17,8 @@ import { ChannelController } from './integrations/channel/channel.controller';
|
||||
import { MetaController } from './integrations/channel/meta/meta.controller';
|
||||
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
|
||||
import { ChatbotController } from './integrations/chatbot/chatbot.controller';
|
||||
import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller';
|
||||
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
|
||||
import { DifyService } from './integrations/chatbot/dify/services/dify.service';
|
||||
import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller';
|
||||
import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service';
|
||||
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
|
||||
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
|
||||
import { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
|
||||
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
|
||||
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
|
||||
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
|
||||
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
|
||||
import { TypebotService } from './integrations/chatbot/typebot/services/typebot.service';
|
||||
import { EventManager } from './integrations/event/event.manager';
|
||||
import { S3Controller } from './integrations/storage/s3/controllers/s3.controller';
|
||||
import { S3Service } from './integrations/storage/s3/services/s3.service';
|
||||
@ -44,11 +32,6 @@ import { TemplateService } from './services/template.service';
|
||||
|
||||
const logger = new Logger('WA MODULE');
|
||||
|
||||
let chatwootCache: CacheService = null;
|
||||
if (configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
|
||||
}
|
||||
|
||||
export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
|
||||
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
|
||||
|
||||
@ -65,7 +48,6 @@ export const waMonitor = new WAMonitoringService(
|
||||
prismaRepository,
|
||||
providerFiles,
|
||||
cache,
|
||||
chatwootCache,
|
||||
baileysCache,
|
||||
);
|
||||
|
||||
@ -78,9 +60,6 @@ export const templateController = new TemplateController(templateService);
|
||||
const proxyService = new ProxyService(waMonitor);
|
||||
export const proxyController = new ProxyController(proxyService, waMonitor);
|
||||
|
||||
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
|
||||
export const chatwootController = new ChatwootController(chatwootService, configService, prismaRepository);
|
||||
|
||||
const settingsService = new SettingsService(waMonitor);
|
||||
export const settingsController = new SettingsController(settingsService);
|
||||
|
||||
@ -89,11 +68,9 @@ export const instanceController = new InstanceController(
|
||||
configService,
|
||||
prismaRepository,
|
||||
eventEmitter,
|
||||
chatwootService,
|
||||
settingsService,
|
||||
proxyController,
|
||||
cache,
|
||||
chatwootCache,
|
||||
baileysCache,
|
||||
providerFiles,
|
||||
);
|
||||
@ -112,23 +89,8 @@ export const channelController = new ChannelController(prismaRepository, waMonit
|
||||
export const metaController = new MetaController(prismaRepository, waMonitor);
|
||||
export const baileysController = new BaileysController(waMonitor);
|
||||
|
||||
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
|
||||
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
|
||||
|
||||
// chatbots
|
||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
|
||||
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
|
||||
|
||||
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
|
||||
|
||||
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
|
||||
|
||||
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
|
||||
const n8nService = new N8nService(waMonitor, prismaRepository, configService);
|
||||
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
|
||||
|
||||
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService, openaiService);
|
||||
export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor);
|
||||
|
||||
logger.info('Module - ON');
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ProxyDto } from '@api/dto/proxy.dto';
|
||||
import { SettingsDto } from '@api/dto/settings.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
import { PrismaRepository, Query } from '@api/repository/repository.service';
|
||||
import { eventManager, waMonitor } from '@api/server.module';
|
||||
import { eventManager } from '@api/server.module';
|
||||
import { Events, wa } from '@api/types/wa.types';
|
||||
import { Auth, Chatwoot, ConfigService, HttpServer, Proxy } from '@config/env.config';
|
||||
import { Auth, ConfigService, HttpServer, Proxy } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { NotFoundException } from '@exceptions';
|
||||
import { Contact, Message, Prisma } from '@prisma/client';
|
||||
@ -19,38 +14,21 @@ import { isArray } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
export class ChannelStartupService {
|
||||
constructor(
|
||||
public readonly configService: ConfigService,
|
||||
public readonly eventEmitter: EventEmitter2,
|
||||
public readonly prismaRepository: PrismaRepository,
|
||||
public readonly chatwootCache: CacheService,
|
||||
) {}
|
||||
|
||||
public readonly logger = new Logger('ChannelStartupService');
|
||||
|
||||
public client: WASocket;
|
||||
public readonly instance: wa.Instance = {};
|
||||
public readonly localChatwoot: wa.LocalChatwoot = {};
|
||||
public readonly localProxy: wa.LocalProxy = {};
|
||||
public readonly localSettings: wa.LocalSettings = {};
|
||||
public readonly localWebhook: wa.LocalWebHook = {};
|
||||
|
||||
public chatwootService = new ChatwootService(
|
||||
waMonitor,
|
||||
this.configService,
|
||||
this.prismaRepository,
|
||||
this.chatwootCache,
|
||||
);
|
||||
|
||||
public openaiService = new OpenaiService(waMonitor, this.prismaRepository, this.configService);
|
||||
|
||||
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
|
||||
|
||||
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceName);
|
||||
|
||||
@ -60,17 +38,6 @@ export class ChannelStartupService {
|
||||
this.instance.number = instance.number;
|
||||
this.instance.token = instance.token;
|
||||
this.instance.businessId = instance.businessId;
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
Events.STATUS_INSTANCE,
|
||||
{ instanceName: this.instance.name },
|
||||
{
|
||||
instance: this.instance.name,
|
||||
status: 'created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public set instanceName(name: string) {
|
||||
@ -221,146 +188,6 @@ export class ChannelStartupService {
|
||||
};
|
||||
}
|
||||
|
||||
public async loadChatwoot() {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.prismaRepository.chatwoot.findUnique({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
this.localChatwoot.enabled = data?.enabled;
|
||||
this.localChatwoot.accountId = data?.accountId;
|
||||
this.localChatwoot.token = data?.token;
|
||||
this.localChatwoot.url = data?.url;
|
||||
this.localChatwoot.nameInbox = data?.nameInbox;
|
||||
this.localChatwoot.signMsg = data?.signMsg;
|
||||
this.localChatwoot.signDelimiter = data?.signDelimiter;
|
||||
this.localChatwoot.number = data?.number;
|
||||
this.localChatwoot.reopenConversation = data?.reopenConversation;
|
||||
this.localChatwoot.conversationPending = data?.conversationPending;
|
||||
this.localChatwoot.mergeBrazilContacts = data?.mergeBrazilContacts;
|
||||
this.localChatwoot.importContacts = data?.importContacts;
|
||||
this.localChatwoot.importMessages = data?.importMessages;
|
||||
this.localChatwoot.daysLimitImportMessages = data?.daysLimitImportMessages;
|
||||
}
|
||||
|
||||
public async setChatwoot(data: ChatwootDto) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatwoot = await this.prismaRepository.chatwoot.findUnique({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (chatwoot) {
|
||||
await this.prismaRepository.chatwoot.update({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
accountId: data.accountId,
|
||||
token: data.token,
|
||||
url: data.url,
|
||||
nameInbox: data.nameInbox,
|
||||
signMsg: data.signMsg,
|
||||
signDelimiter: data.signMsg ? data.signDelimiter : null,
|
||||
number: data.number,
|
||||
reopenConversation: data.reopenConversation,
|
||||
conversationPending: data.conversationPending,
|
||||
mergeBrazilContacts: data.mergeBrazilContacts,
|
||||
importContacts: data.importContacts,
|
||||
importMessages: data.importMessages,
|
||||
daysLimitImportMessages: data.daysLimitImportMessages,
|
||||
organization: data.organization,
|
||||
logo: data.logo,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null });
|
||||
|
||||
this.clearCacheChatwoot();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.chatwoot.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
accountId: data.accountId,
|
||||
token: data.token,
|
||||
url: data.url,
|
||||
nameInbox: data.nameInbox,
|
||||
signMsg: data.signMsg,
|
||||
number: data.number,
|
||||
reopenConversation: data.reopenConversation,
|
||||
conversationPending: data.conversationPending,
|
||||
mergeBrazilContacts: data.mergeBrazilContacts,
|
||||
importContacts: data.importContacts,
|
||||
importMessages: data.importMessages,
|
||||
daysLimitImportMessages: data.daysLimitImportMessages,
|
||||
organization: data.organization,
|
||||
logo: data.logo,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null });
|
||||
|
||||
this.clearCacheChatwoot();
|
||||
}
|
||||
|
||||
public async findChatwoot(): Promise<ChatwootDto | null> {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await this.prismaRepository.chatwoot.findUnique({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ignoreJidsArray = Array.isArray(data.ignoreJids) ? data.ignoreJids.map((event) => String(event)) : [];
|
||||
|
||||
return {
|
||||
enabled: data?.enabled,
|
||||
accountId: data.accountId,
|
||||
token: data.token,
|
||||
url: data.url,
|
||||
nameInbox: data.nameInbox,
|
||||
signMsg: data.signMsg,
|
||||
signDelimiter: data.signDelimiter || null,
|
||||
reopenConversation: data.reopenConversation,
|
||||
conversationPending: data.conversationPending,
|
||||
mergeBrazilContacts: data.mergeBrazilContacts,
|
||||
importContacts: data.importContacts,
|
||||
importMessages: data.importMessages,
|
||||
daysLimitImportMessages: data.daysLimitImportMessages,
|
||||
organization: data.organization,
|
||||
logo: data.logo,
|
||||
ignoreJids: ignoreJidsArray,
|
||||
};
|
||||
}
|
||||
|
||||
public clearCacheChatwoot() {
|
||||
if (this.localChatwoot?.enabled) {
|
||||
this.chatwootService.getCache()?.deleteAll(this.instanceName);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadProxy() {
|
||||
this.localProxy.enabled = false;
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { ProviderFiles } from '@api/provider/sessions';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { channelController } from '@api/server.module';
|
||||
import { Events, Integration } from '@api/types/wa.types';
|
||||
import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
|
||||
import { CacheConf, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
|
||||
import { NotFoundException } from '@exceptions';
|
||||
@ -21,7 +21,6 @@ export class WAMonitoringService {
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly baileysCache: CacheService,
|
||||
) {
|
||||
this.removeInstance();
|
||||
@ -90,7 +89,6 @@ export class WAMonitoringService {
|
||||
const instances = await this.prismaRepository.instance.findMany({
|
||||
where,
|
||||
include: {
|
||||
Chatwoot: true,
|
||||
Proxy: true,
|
||||
Rabbitmq: true,
|
||||
Nats: true,
|
||||
@ -170,11 +168,6 @@ export class WAMonitoringService {
|
||||
}
|
||||
|
||||
public async cleaningStoreData(instanceName: string) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
|
||||
execFileSync('rm', ['-rf', instancePath]);
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({
|
||||
where: { name: instanceName },
|
||||
});
|
||||
@ -191,13 +184,11 @@ export class WAMonitoringService {
|
||||
await this.prismaRepository.message.deleteMany({ where: { instanceId: instance.id } });
|
||||
|
||||
await this.prismaRepository.webhook.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.websocket.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.setting.deleteMany({ where: { instanceId: instance.id } });
|
||||
await this.prismaRepository.label.deleteMany({ where: { instanceId: instance.id } });
|
||||
@ -257,7 +248,6 @@ export class WAMonitoringService {
|
||||
eventEmitter: this.eventEmitter,
|
||||
prismaRepository: this.prismaRepository,
|
||||
cache: this.cache,
|
||||
chatwootCache: this.chatwootCache,
|
||||
baileysCache: this.baileysCache,
|
||||
providerFiles: this.providerFiles,
|
||||
});
|
||||
@ -377,10 +367,6 @@ export class WAMonitoringService {
|
||||
try {
|
||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
this.waInstances[instanceName]?.clearCacheChatwoot();
|
||||
}
|
||||
|
||||
this.cleaningUp(instanceName);
|
||||
} finally {
|
||||
this.logger.warn(`Instance "${instanceName}" - LOGOUT`);
|
||||
|
||||
@ -28,8 +28,6 @@ export enum Events {
|
||||
GROUPS_UPDATE = 'groups.update',
|
||||
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
|
||||
CALL = 'call',
|
||||
TYPEBOT_START = 'typebot.start',
|
||||
TYPEBOT_CHANGE_STATUS = 'typebot.change-status',
|
||||
LABELS_EDIT = 'labels.edit',
|
||||
LABELS_ASSOCIATION = 'labels.association',
|
||||
CREDS_UPDATE = 'creds.update',
|
||||
@ -61,23 +59,6 @@ export declare namespace wa {
|
||||
businessId?: string;
|
||||
};
|
||||
|
||||
export type LocalChatwoot = {
|
||||
enabled?: boolean;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
nameInbox?: string;
|
||||
signMsg?: boolean;
|
||||
signDelimiter?: string;
|
||||
number?: string;
|
||||
reopenConversation?: boolean;
|
||||
conversationPending?: boolean;
|
||||
mergeBrazilContacts?: boolean;
|
||||
importContacts?: boolean;
|
||||
importMessages?: boolean;
|
||||
daysLimitImportMessages?: number;
|
||||
};
|
||||
|
||||
export type LocalSettings = {
|
||||
rejectCall?: boolean;
|
||||
msgCall?: string;
|
||||
|
||||
@ -84,8 +84,6 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -121,8 +119,6 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -158,8 +154,6 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -195,23 +189,9 @@ export const instanceSchema: JSONSchema7 = {
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Chatwoot
|
||||
chatwootAccountId: { type: 'string' },
|
||||
chatwootToken: { type: 'string' },
|
||||
chatwootUrl: { type: 'string' },
|
||||
chatwootSignMsg: { type: 'boolean' },
|
||||
chatwootReopenConversation: { type: 'boolean' },
|
||||
chatwootConversationPending: { type: 'boolean' },
|
||||
chatwootImportContacts: { type: 'boolean' },
|
||||
chatwootNameInbox: { type: 'string' },
|
||||
chatwootMergeBrazilContacts: { type: 'boolean' },
|
||||
chatwootImportMessages: { type: 'boolean' },
|
||||
chatwootDaysLimitImportMessages: { type: 'number' },
|
||||
},
|
||||
...isNotEmpty('instanceName'),
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user