mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-12 03:19:37 -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
|
regex
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OpenaiBotType {
|
|
||||||
assistant
|
|
||||||
chatCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DifyBotType {
|
|
||||||
chatBot
|
|
||||||
textGenerator
|
|
||||||
agent
|
|
||||||
workflow
|
|
||||||
}
|
|
||||||
|
|
||||||
model Instance {
|
model Instance {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique @db.VarChar(255)
|
name String @unique @db.VarChar(255)
|
||||||
|
|||||||
@ -48,18 +48,6 @@ enum TriggerOperator {
|
|||||||
regex
|
regex
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OpenaiBotType {
|
|
||||||
assistant
|
|
||||||
chatCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DifyBotType {
|
|
||||||
chatBot
|
|
||||||
textGenerator
|
|
||||||
agent
|
|
||||||
workflow
|
|
||||||
}
|
|
||||||
|
|
||||||
model Instance {
|
model Instance {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique @db.VarChar(255)
|
name String @unique @db.VarChar(255)
|
||||||
@ -81,7 +69,6 @@ model Instance {
|
|||||||
Contact Contact[]
|
Contact Contact[]
|
||||||
Message Message[]
|
Message Message[]
|
||||||
Webhook Webhook?
|
Webhook Webhook?
|
||||||
Chatwoot Chatwoot?
|
|
||||||
Label Label[]
|
Label Label[]
|
||||||
Proxy Proxy?
|
Proxy Proxy?
|
||||||
Setting Setting?
|
Setting Setting?
|
||||||
@ -90,25 +77,14 @@ model Instance {
|
|||||||
Sqs Sqs?
|
Sqs Sqs?
|
||||||
Kafka Kafka?
|
Kafka Kafka?
|
||||||
Websocket Websocket?
|
Websocket Websocket?
|
||||||
Typebot Typebot[]
|
|
||||||
Session Session?
|
Session Session?
|
||||||
MessageUpdate MessageUpdate[]
|
MessageUpdate MessageUpdate[]
|
||||||
TypebotSetting TypebotSetting?
|
|
||||||
Media Media[]
|
Media Media[]
|
||||||
OpenaiCreds OpenaiCreds[]
|
|
||||||
OpenaiBot OpenaiBot[]
|
|
||||||
OpenaiSetting OpenaiSetting?
|
|
||||||
Template Template[]
|
Template Template[]
|
||||||
Dify Dify[]
|
|
||||||
DifySetting DifySetting?
|
|
||||||
IntegrationSession IntegrationSession[]
|
IntegrationSession IntegrationSession[]
|
||||||
Flowise Flowise[]
|
|
||||||
FlowiseSetting FlowiseSetting?
|
|
||||||
Pusher Pusher?
|
Pusher Pusher?
|
||||||
N8n N8n[]
|
N8n N8n[]
|
||||||
N8nSetting N8nSetting[]
|
N8nSetting N8nSetting[]
|
||||||
Evoai Evoai[]
|
|
||||||
EvoaiSetting EvoaiSetting?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
@ -159,11 +135,6 @@ model Message {
|
|||||||
contextInfo Json? @db.JsonB
|
contextInfo Json? @db.JsonB
|
||||||
source DeviceMessage
|
source DeviceMessage
|
||||||
messageTimestamp Int @db.Integer
|
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)
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
instanceId String
|
instanceId String
|
||||||
MessageUpdate MessageUpdate[]
|
MessageUpdate MessageUpdate[]
|
||||||
@ -210,31 +181,6 @@ model Webhook {
|
|||||||
@@index([instanceId])
|
@@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 {
|
model Label {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
labelId String? @db.VarChar(100)
|
labelId String? @db.VarChar(100)
|
||||||
@ -346,54 +292,6 @@ model Pusher {
|
|||||||
instanceId String @unique
|
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 {
|
model Media {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
fileName String @db.VarChar(500)
|
fileName String @db.VarChar(500)
|
||||||
@ -406,53 +304,6 @@ model Media {
|
|||||||
instanceId String
|
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 {
|
model IntegrationSession {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
sessionId String @db.VarChar(255)
|
sessionId String @db.VarChar(255)
|
||||||
@ -472,30 +323,6 @@ model IntegrationSession {
|
|||||||
botId String?
|
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 {
|
model Template {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
templateId String @unique @db.VarChar(255)
|
templateId String @unique @db.VarChar(255)
|
||||||
@ -508,103 +335,6 @@ model Template {
|
|||||||
instanceId String
|
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 {
|
model IsOnWhatsapp {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
remoteJid String @unique @db.VarChar(100)
|
remoteJid String @unique @db.VarChar(100)
|
||||||
@ -662,51 +392,3 @@ model N8nSetting {
|
|||||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||||
instanceId String @unique
|
instanceId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model 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;
|
byEvents?: boolean;
|
||||||
base64?: 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 {
|
export class SetPresenceDto {
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { PrismaRepository } from '@api/repository/repository.service';
|
import { PrismaRepository } from '@api/repository/repository.service';
|
||||||
import {
|
import { n8nController } from '@api/server.module';
|
||||||
difyController,
|
|
||||||
evoaiController,
|
|
||||||
flowiseController,
|
|
||||||
n8nController,
|
|
||||||
openaiController,
|
|
||||||
typebotController,
|
|
||||||
} from '@api/server.module';
|
|
||||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||||
import { Logger } from '@config/logger.config';
|
import { Logger } from '@config/logger.config';
|
||||||
import { IntegrationSession } from '@prisma/client';
|
import { IntegrationSession } from '@prisma/client';
|
||||||
@ -91,17 +84,7 @@ export class ChatbotController {
|
|||||||
isIntegration,
|
isIntegration,
|
||||||
};
|
};
|
||||||
|
|
||||||
typebotController.emit(emitData);
|
|
||||||
|
|
||||||
openaiController.emit(emitData);
|
|
||||||
|
|
||||||
difyController.emit(emitData);
|
|
||||||
|
|
||||||
n8nController.emit(emitData);
|
n8nController.emit(emitData);
|
||||||
|
|
||||||
evoaiController.emit(emitData);
|
|
||||||
|
|
||||||
flowiseController.emit(emitData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public processDebounce(
|
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 { Router } from 'express';
|
||||||
|
|
||||||
import { EvoaiRouter } from './evoai/routes/evoai.router';
|
|
||||||
import { FlowiseRouter } from './flowise/routes/flowise.router';
|
|
||||||
import { N8nRouter } from './n8n/routes/n8n.router';
|
import { N8nRouter } from './n8n/routes/n8n.router';
|
||||||
|
|
||||||
export class ChatbotRouter {
|
export class ChatbotRouter {
|
||||||
@ -14,12 +8,6 @@ export class ChatbotRouter {
|
|||||||
constructor(...guards: any[]) {
|
constructor(...guards: any[]) {
|
||||||
this.router = Router();
|
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('/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/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 axios from 'axios';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
import { OpenaiService } from '../../openai/services/openai.service';
|
|
||||||
|
|
||||||
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
||||||
private openaiService: OpenaiService;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
waMonitor: WAMonitoringService,
|
waMonitor: WAMonitoringService,
|
||||||
prismaRepository: PrismaRepository,
|
prismaRepository: PrismaRepository,
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
openaiService: OpenaiService,
|
|
||||||
) {
|
) {
|
||||||
super(waMonitor, prismaRepository, 'N8nService', configService);
|
super(waMonitor, prismaRepository, 'N8nService', configService);
|
||||||
this.openaiService = openaiService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,18 +51,8 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
|||||||
apiKey: instance.token,
|
apiKey: instance.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle audio messages
|
// Audio transcription removed due to OpenAI integration removal
|
||||||
if (this.isAudioMessage(content) && msg) {
|
// Audio messages will be sent as-is without transcription
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (n8n.basicAuthUser && n8n.basicAuthPass) {
|
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';
|
import { EventInstanceMixin } from '@api/integrations/event/event.dto';
|
||||||
|
|
||||||
export type Constructor<T = {}> = new (...args: any[]) => T;
|
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 { 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 { eventEmitter } from '@config/event.config';
|
||||||
import { Logger } from '@config/logger.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 { MetaController } from './integrations/channel/meta/meta.controller';
|
||||||
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
|
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
|
||||||
import { ChatbotController } from './integrations/chatbot/chatbot.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 { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
|
||||||
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
|
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 { EventManager } from './integrations/event/event.manager';
|
||||||
import { S3Controller } from './integrations/storage/s3/controllers/s3.controller';
|
import { S3Controller } from './integrations/storage/s3/controllers/s3.controller';
|
||||||
import { S3Service } from './integrations/storage/s3/services/s3.service';
|
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');
|
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());
|
export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
|
||||||
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
|
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
|
||||||
|
|
||||||
@ -65,7 +48,6 @@ export const waMonitor = new WAMonitoringService(
|
|||||||
prismaRepository,
|
prismaRepository,
|
||||||
providerFiles,
|
providerFiles,
|
||||||
cache,
|
cache,
|
||||||
chatwootCache,
|
|
||||||
baileysCache,
|
baileysCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -78,9 +60,6 @@ export const templateController = new TemplateController(templateService);
|
|||||||
const proxyService = new ProxyService(waMonitor);
|
const proxyService = new ProxyService(waMonitor);
|
||||||
export const proxyController = new ProxyController(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);
|
const settingsService = new SettingsService(waMonitor);
|
||||||
export const settingsController = new SettingsController(settingsService);
|
export const settingsController = new SettingsController(settingsService);
|
||||||
|
|
||||||
@ -89,11 +68,9 @@ export const instanceController = new InstanceController(
|
|||||||
configService,
|
configService,
|
||||||
prismaRepository,
|
prismaRepository,
|
||||||
eventEmitter,
|
eventEmitter,
|
||||||
chatwootService,
|
|
||||||
settingsService,
|
settingsService,
|
||||||
proxyController,
|
proxyController,
|
||||||
cache,
|
cache,
|
||||||
chatwootCache,
|
|
||||||
baileysCache,
|
baileysCache,
|
||||||
providerFiles,
|
providerFiles,
|
||||||
);
|
);
|
||||||
@ -112,23 +89,8 @@ export const channelController = new ChannelController(prismaRepository, waMonit
|
|||||||
export const metaController = new MetaController(prismaRepository, waMonitor);
|
export const metaController = new MetaController(prismaRepository, waMonitor);
|
||||||
export const baileysController = new BaileysController(waMonitor);
|
export const baileysController = new BaileysController(waMonitor);
|
||||||
|
|
||||||
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
|
|
||||||
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
|
|
||||||
|
|
||||||
// chatbots
|
// chatbots
|
||||||
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
|
const n8nService = new N8nService(waMonitor, prismaRepository, configService);
|
||||||
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);
|
|
||||||
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
|
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');
|
logger.info('Module - ON');
|
||||||
|
|||||||
@ -1,15 +1,10 @@
|
|||||||
import { InstanceDto } from '@api/dto/instance.dto';
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { ProxyDto } from '@api/dto/proxy.dto';
|
import { ProxyDto } from '@api/dto/proxy.dto';
|
||||||
import { SettingsDto } from '@api/dto/settings.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 { 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 { 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 { Logger } from '@config/logger.config';
|
||||||
import { NotFoundException } from '@exceptions';
|
import { NotFoundException } from '@exceptions';
|
||||||
import { Contact, Message, Prisma } from '@prisma/client';
|
import { Contact, Message, Prisma } from '@prisma/client';
|
||||||
@ -19,38 +14,21 @@ import { isArray } from 'class-validator';
|
|||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { CacheService } from './cache.service';
|
|
||||||
|
|
||||||
export class ChannelStartupService {
|
export class ChannelStartupService {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly configService: ConfigService,
|
public readonly configService: ConfigService,
|
||||||
public readonly eventEmitter: EventEmitter2,
|
public readonly eventEmitter: EventEmitter2,
|
||||||
public readonly prismaRepository: PrismaRepository,
|
public readonly prismaRepository: PrismaRepository,
|
||||||
public readonly chatwootCache: CacheService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public readonly logger = new Logger('ChannelStartupService');
|
public readonly logger = new Logger('ChannelStartupService');
|
||||||
|
|
||||||
public client: WASocket;
|
public client: WASocket;
|
||||||
public readonly instance: wa.Instance = {};
|
public readonly instance: wa.Instance = {};
|
||||||
public readonly localChatwoot: wa.LocalChatwoot = {};
|
|
||||||
public readonly localProxy: wa.LocalProxy = {};
|
public readonly localProxy: wa.LocalProxy = {};
|
||||||
public readonly localSettings: wa.LocalSettings = {};
|
public readonly localSettings: wa.LocalSettings = {};
|
||||||
public readonly localWebhook: wa.LocalWebHook = {};
|
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) {
|
public setInstance(instance: InstanceDto) {
|
||||||
this.logger.setInstance(instance.instanceName);
|
this.logger.setInstance(instance.instanceName);
|
||||||
|
|
||||||
@ -60,17 +38,6 @@ export class ChannelStartupService {
|
|||||||
this.instance.number = instance.number;
|
this.instance.number = instance.number;
|
||||||
this.instance.token = instance.token;
|
this.instance.token = instance.token;
|
||||||
this.instance.businessId = instance.businessId;
|
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) {
|
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() {
|
public async loadProxy() {
|
||||||
this.localProxy.enabled = false;
|
this.localProxy.enabled = false;
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ProviderFiles } from '@api/provider/sessions';
|
|||||||
import { PrismaRepository } from '@api/repository/repository.service';
|
import { PrismaRepository } from '@api/repository/repository.service';
|
||||||
import { channelController } from '@api/server.module';
|
import { channelController } from '@api/server.module';
|
||||||
import { Events, Integration } from '@api/types/wa.types';
|
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 { Logger } from '@config/logger.config';
|
||||||
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
|
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
|
||||||
import { NotFoundException } from '@exceptions';
|
import { NotFoundException } from '@exceptions';
|
||||||
@ -21,7 +21,6 @@ export class WAMonitoringService {
|
|||||||
private readonly prismaRepository: PrismaRepository,
|
private readonly prismaRepository: PrismaRepository,
|
||||||
private readonly providerFiles: ProviderFiles,
|
private readonly providerFiles: ProviderFiles,
|
||||||
private readonly cache: CacheService,
|
private readonly cache: CacheService,
|
||||||
private readonly chatwootCache: CacheService,
|
|
||||||
private readonly baileysCache: CacheService,
|
private readonly baileysCache: CacheService,
|
||||||
) {
|
) {
|
||||||
this.removeInstance();
|
this.removeInstance();
|
||||||
@ -90,7 +89,6 @@ export class WAMonitoringService {
|
|||||||
const instances = await this.prismaRepository.instance.findMany({
|
const instances = await this.prismaRepository.instance.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
Chatwoot: true,
|
|
||||||
Proxy: true,
|
Proxy: true,
|
||||||
Rabbitmq: true,
|
Rabbitmq: true,
|
||||||
Nats: true,
|
Nats: true,
|
||||||
@ -170,11 +168,6 @@ export class WAMonitoringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async cleaningStoreData(instanceName: string) {
|
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({
|
const instance = await this.prismaRepository.instance.findFirst({
|
||||||
where: { name: instanceName },
|
where: { name: instanceName },
|
||||||
});
|
});
|
||||||
@ -191,13 +184,11 @@ export class WAMonitoringService {
|
|||||||
await this.prismaRepository.message.deleteMany({ where: { instanceId: instance.id } });
|
await this.prismaRepository.message.deleteMany({ where: { instanceId: instance.id } });
|
||||||
|
|
||||||
await this.prismaRepository.webhook.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.proxy.deleteMany({ where: { instanceId: instance.id } });
|
||||||
await this.prismaRepository.rabbitmq.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.nats.deleteMany({ where: { instanceId: instance.id } });
|
||||||
await this.prismaRepository.sqs.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.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.websocket.deleteMany({ where: { instanceId: instance.id } });
|
||||||
await this.prismaRepository.setting.deleteMany({ where: { instanceId: instance.id } });
|
await this.prismaRepository.setting.deleteMany({ where: { instanceId: instance.id } });
|
||||||
await this.prismaRepository.label.deleteMany({ where: { instanceId: instance.id } });
|
await this.prismaRepository.label.deleteMany({ where: { instanceId: instance.id } });
|
||||||
@ -257,7 +248,6 @@ export class WAMonitoringService {
|
|||||||
eventEmitter: this.eventEmitter,
|
eventEmitter: this.eventEmitter,
|
||||||
prismaRepository: this.prismaRepository,
|
prismaRepository: this.prismaRepository,
|
||||||
cache: this.cache,
|
cache: this.cache,
|
||||||
chatwootCache: this.chatwootCache,
|
|
||||||
baileysCache: this.baileysCache,
|
baileysCache: this.baileysCache,
|
||||||
providerFiles: this.providerFiles,
|
providerFiles: this.providerFiles,
|
||||||
});
|
});
|
||||||
@ -377,10 +367,6 @@ export class WAMonitoringService {
|
|||||||
try {
|
try {
|
||||||
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
|
||||||
|
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
|
||||||
this.waInstances[instanceName]?.clearCacheChatwoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleaningUp(instanceName);
|
this.cleaningUp(instanceName);
|
||||||
} finally {
|
} finally {
|
||||||
this.logger.warn(`Instance "${instanceName}" - LOGOUT`);
|
this.logger.warn(`Instance "${instanceName}" - LOGOUT`);
|
||||||
|
|||||||
@ -28,8 +28,6 @@ export enum Events {
|
|||||||
GROUPS_UPDATE = 'groups.update',
|
GROUPS_UPDATE = 'groups.update',
|
||||||
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
|
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
|
||||||
CALL = 'call',
|
CALL = 'call',
|
||||||
TYPEBOT_START = 'typebot.start',
|
|
||||||
TYPEBOT_CHANGE_STATUS = 'typebot.change-status',
|
|
||||||
LABELS_EDIT = 'labels.edit',
|
LABELS_EDIT = 'labels.edit',
|
||||||
LABELS_ASSOCIATION = 'labels.association',
|
LABELS_ASSOCIATION = 'labels.association',
|
||||||
CREDS_UPDATE = 'creds.update',
|
CREDS_UPDATE = 'creds.update',
|
||||||
@ -61,23 +59,6 @@ export declare namespace wa {
|
|||||||
businessId?: string;
|
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 = {
|
export type LocalSettings = {
|
||||||
rejectCall?: boolean;
|
rejectCall?: boolean;
|
||||||
msgCall?: string;
|
msgCall?: string;
|
||||||
|
|||||||
@ -84,8 +84,6 @@ export const instanceSchema: JSONSchema7 = {
|
|||||||
'LABELS_EDIT',
|
'LABELS_EDIT',
|
||||||
'LABELS_ASSOCIATION',
|
'LABELS_ASSOCIATION',
|
||||||
'CALL',
|
'CALL',
|
||||||
'TYPEBOT_START',
|
|
||||||
'TYPEBOT_CHANGE_STATUS',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -121,8 +119,6 @@ export const instanceSchema: JSONSchema7 = {
|
|||||||
'LABELS_EDIT',
|
'LABELS_EDIT',
|
||||||
'LABELS_ASSOCIATION',
|
'LABELS_ASSOCIATION',
|
||||||
'CALL',
|
'CALL',
|
||||||
'TYPEBOT_START',
|
|
||||||
'TYPEBOT_CHANGE_STATUS',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -158,8 +154,6 @@ export const instanceSchema: JSONSchema7 = {
|
|||||||
'LABELS_EDIT',
|
'LABELS_EDIT',
|
||||||
'LABELS_ASSOCIATION',
|
'LABELS_ASSOCIATION',
|
||||||
'CALL',
|
'CALL',
|
||||||
'TYPEBOT_START',
|
|
||||||
'TYPEBOT_CHANGE_STATUS',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -195,23 +189,9 @@ export const instanceSchema: JSONSchema7 = {
|
|||||||
'LABELS_EDIT',
|
'LABELS_EDIT',
|
||||||
'LABELS_ASSOCIATION',
|
'LABELS_ASSOCIATION',
|
||||||
'CALL',
|
'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'),
|
...isNotEmpty('instanceName'),
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user