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:
Claude 2025-11-09 07:40:22 +00:00
parent 8884ef42d0
commit 2606dbdac3
No known key found for this signature in database
45 changed files with 9 additions and 9039 deletions

View File

@ -48,18 +48,6 @@ enum TriggerOperator {
regex
}
enum OpenaiBotType {
assistant
chatCompletion
}
enum DifyBotType {
chatBot
textGenerator
agent
workflow
}
model Instance {
id String @id @default(cuid())
name String @unique @db.VarChar(255)

View File

@ -48,18 +48,6 @@ enum TriggerOperator {
regex
}
enum OpenaiBotType {
assistant
chatCompletion
}
enum DifyBotType {
chatBot
textGenerator
agent
workflow
}
model Instance {
id String @id @default(cuid())
name String @unique @db.VarChar(255)
@ -81,7 +69,6 @@ model Instance {
Contact Contact[]
Message Message[]
Webhook Webhook?
Chatwoot Chatwoot?
Label Label[]
Proxy Proxy?
Setting Setting?
@ -90,25 +77,14 @@ model Instance {
Sqs Sqs?
Kafka Kafka?
Websocket Websocket?
Typebot Typebot[]
Session Session?
MessageUpdate MessageUpdate[]
TypebotSetting TypebotSetting?
Media Media[]
OpenaiCreds OpenaiCreds[]
OpenaiBot OpenaiBot[]
OpenaiSetting OpenaiSetting?
Template Template[]
Dify Dify[]
DifySetting DifySetting?
IntegrationSession IntegrationSession[]
Flowise Flowise[]
FlowiseSetting FlowiseSetting?
Pusher Pusher?
N8n N8n[]
N8nSetting N8nSetting[]
Evoai Evoai[]
EvoaiSetting EvoaiSetting?
}
model Session {
@ -159,11 +135,6 @@ model Message {
contextInfo Json? @db.JsonB
source DeviceMessage
messageTimestamp Int @db.Integer
chatwootMessageId Int? @db.Integer
chatwootInboxId Int? @db.Integer
chatwootConversationId Int? @db.Integer
chatwootContactInboxSourceId String? @db.VarChar(100)
chatwootIsRead Boolean? @db.Boolean
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
MessageUpdate MessageUpdate[]
@ -210,31 +181,6 @@ model Webhook {
@@index([instanceId])
}
model Chatwoot {
id String @id @default(cuid())
enabled Boolean? @default(true) @db.Boolean
accountId String? @db.VarChar(100)
token String? @db.VarChar(100)
url String? @db.VarChar(500)
nameInbox String? @db.VarChar(100)
signMsg Boolean? @default(false) @db.Boolean
signDelimiter String? @db.VarChar(100)
number String? @db.VarChar(100)
reopenConversation Boolean? @default(false) @db.Boolean
conversationPending Boolean? @default(false) @db.Boolean
mergeBrazilContacts Boolean? @default(false) @db.Boolean
importContacts Boolean? @default(false) @db.Boolean
importMessages Boolean? @default(false) @db.Boolean
daysLimitImportMessages Int? @db.Integer
organization String? @db.VarChar(100)
logo String? @db.VarChar(500)
ignoreJids Json?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Label {
id String @id @default(cuid())
labelId String? @db.VarChar(100)
@ -346,54 +292,6 @@ model Pusher {
instanceId String @unique
}
model Typebot {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
url String @db.VarChar(500)
typebot String @db.VarChar(100)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp
ignoreJids Json?
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
TypebotSetting TypebotSetting[]
}
model TypebotSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
typebotIdFallback String? @db.VarChar(100)
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Media {
id String @id @default(cuid())
fileName String @db.VarChar(500)
@ -406,53 +304,6 @@ model Media {
instanceId String
}
model OpenaiCreds {
id String @id @default(cuid())
name String? @unique @db.VarChar(255)
apiKey String? @unique @db.VarChar(255)
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
OpenaiAssistant OpenaiBot[]
OpenaiSetting OpenaiSetting?
}
model OpenaiBot {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
botType OpenaiBotType
assistantId String? @db.VarChar(255)
functionUrl String? @db.VarChar(500)
model String? @db.VarChar(100)
systemMessages Json? @db.JsonB
assistantMessages Json? @db.JsonB
userMessages Json? @db.JsonB
maxTokens Int? @db.Integer
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
ignoreJids Json?
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
openaiCredsId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
OpenaiSetting OpenaiSetting[]
}
model IntegrationSession {
id String @id @default(cuid())
sessionId String @db.VarChar(255)
@ -472,30 +323,6 @@ model IntegrationSession {
botId String?
}
model OpenaiSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
speechToText Boolean? @default(false) @db.Boolean
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id])
openaiCredsId String @unique
Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id])
openaiIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Template {
id String @id @default(cuid())
templateId String @unique @db.VarChar(255)
@ -508,103 +335,6 @@ model Template {
instanceId String
}
model Dify {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
botType DifyBotType
apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
DifySetting DifySetting[]
}
model DifySetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Dify? @relation(fields: [difyIdFallback], references: [id])
difyIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Flowise {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
apiUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
FlowiseSetting FlowiseSetting[]
}
model FlowiseSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Flowise? @relation(fields: [flowiseIdFallback], references: [id])
flowiseIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model IsOnWhatsapp {
id String @id @default(cuid())
remoteJid String @unique @db.VarChar(100)
@ -662,51 +392,3 @@ model N8nSetting {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Evoai {
id String @id @default(cuid())
enabled Boolean @default(true) @db.Boolean
description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
triggerType TriggerType?
triggerOperator TriggerOperator?
triggerValue String?
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
EvoaiSetting EvoaiSetting[]
}
model EvoaiSetting {
id String @id @default(cuid())
expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer
ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean
timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
evoaiIdFallback String? @db.VarChar(100)
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}

View File

@ -37,20 +37,6 @@ export class InstanceDto extends IntegrationDto {
byEvents?: boolean;
base64?: boolean;
};
chatwootAccountId?: string;
chatwootConversationPending?: boolean;
chatwootAutoCreate?: boolean;
chatwootDaysLimitImportMessages?: number;
chatwootImportContacts?: boolean;
chatwootImportMessages?: boolean;
chatwootLogo?: string;
chatwootMergeBrazilContacts?: boolean;
chatwootNameInbox?: string;
chatwootOrganization?: string;
chatwootReopenConversation?: boolean;
chatwootSignMsg?: boolean;
chatwootToken?: string;
chatwootUrl?: string;
}
export class SetPresenceDto {

View File

@ -1,13 +1,6 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import {
difyController,
evoaiController,
flowiseController,
n8nController,
openaiController,
typebotController,
} from '@api/server.module';
import { n8nController } from '@api/server.module';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';
@ -91,17 +84,7 @@ export class ChatbotController {
isIntegration,
};
typebotController.emit(emitData);
openaiController.emit(emitData);
difyController.emit(emitData);
n8nController.emit(emitData);
evoaiController.emit(emitData);
flowiseController.emit(emitData);
}
public processDebounce(

View File

@ -1,11 +1,5 @@
import { ChatwootRouter } from '@api/integrations/chatbot/chatwoot/routes/chatwoot.router';
import { DifyRouter } from '@api/integrations/chatbot/dify/routes/dify.router';
import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.router';
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
import { Router } from 'express';
import { EvoaiRouter } from './evoai/routes/evoai.router';
import { FlowiseRouter } from './flowise/routes/flowise.router';
import { N8nRouter } from './n8n/routes/n8n.router';
export class ChatbotRouter {
@ -14,12 +8,6 @@ export class ChatbotRouter {
constructor(...guards: any[]) {
this.router = Router();
this.router.use('/chatwoot', new ChatwootRouter(...guards).router);
this.router.use('/typebot', new TypebotRouter(...guards).router);
this.router.use('/openai', new OpenaiRouter(...guards).router);
this.router.use('/dify', new DifyRouter(...guards).router);
this.router.use('/flowise', new FlowiseRouter(...guards).router);
this.router.use('/n8n', new N8nRouter(...guards).router);
this.router.use('/evoai', new EvoaiRouter(...guards).router);
}
}

View File

@ -1,7 +1 @@
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';

View File

@ -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);
}
}

View File

@ -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;
};
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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();

View File

@ -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'),
};

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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'),
};

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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'),
};

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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
}

View File

@ -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'),
};

View File

@ -5,19 +5,14 @@ import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
import axios from 'axios';
import { BaseChatbotService } from '../../base-chatbot.service';
import { OpenaiService } from '../../openai/services/openai.service';
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
private openaiService: OpenaiService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
openaiService: OpenaiService,
) {
super(waMonitor, prismaRepository, 'N8nService', configService);
this.openaiService = openaiService;
}
/**
@ -56,18 +51,8 @@ export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
apiKey: instance.token,
};
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
const transcription = await this.openaiService.speechToText(msg, instance);
if (transcription) {
payload.chatInput = `[audio] ${transcription}`;
}
} catch (err) {
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
}
}
// Audio transcription removed due to OpenAI integration removal
// Audio messages will be sent as-is without transcription
const headers: Record<string, string> = {};
if (n8n.basicAuthUser && n8n.basicAuthPass) {

View File

@ -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');
}
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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'),
};

View File

@ -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,
},
},
};
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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'),
};

View File

@ -1,6 +1,5 @@
import { ChatwootInstanceMixin } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
import { EventInstanceMixin } from '@api/integrations/event/event.dto';
export type Constructor<T = {}> = new (...args: any[]) => T;
export class IntegrationDto extends EventInstanceMixin(ChatwootInstanceMixin(class {})) {}
export class IntegrationDto extends EventInstanceMixin(class {}) {}

View File

@ -1,5 +1,5 @@
import { CacheEngine } from '@cache/cacheengine';
import { Chatwoot, configService, ProviderSession } from '@config/env.config';
import { configService, ProviderSession } from '@config/env.config';
import { eventEmitter } from '@config/event.config';
import { Logger } from '@config/logger.config';
@ -17,20 +17,8 @@ import { ChannelController } from './integrations/channel/channel.controller';
import { MetaController } from './integrations/channel/meta/meta.controller';
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
import { ChatbotController } from './integrations/chatbot/chatbot.controller';
import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller';
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
import { DifyController } from './integrations/chatbot/dify/controllers/dify.controller';
import { DifyService } from './integrations/chatbot/dify/services/dify.service';
import { EvoaiController } from './integrations/chatbot/evoai/controllers/evoai.controller';
import { EvoaiService } from './integrations/chatbot/evoai/services/evoai.service';
import { FlowiseController } from './integrations/chatbot/flowise/controllers/flowise.controller';
import { FlowiseService } from './integrations/chatbot/flowise/services/flowise.service';
import { N8nController } from './integrations/chatbot/n8n/controllers/n8n.controller';
import { N8nService } from './integrations/chatbot/n8n/services/n8n.service';
import { OpenaiController } from './integrations/chatbot/openai/controllers/openai.controller';
import { OpenaiService } from './integrations/chatbot/openai/services/openai.service';
import { TypebotController } from './integrations/chatbot/typebot/controllers/typebot.controller';
import { TypebotService } from './integrations/chatbot/typebot/services/typebot.service';
import { EventManager } from './integrations/event/event.manager';
import { S3Controller } from './integrations/storage/s3/controllers/s3.controller';
import { S3Service } from './integrations/storage/s3/services/s3.service';
@ -44,11 +32,6 @@ import { TemplateService } from './services/template.service';
const logger = new Logger('WA MODULE');
let chatwootCache: CacheService = null;
if (configService.get<Chatwoot>('CHATWOOT').ENABLED) {
chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
}
export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
const baileysCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
@ -65,7 +48,6 @@ export const waMonitor = new WAMonitoringService(
prismaRepository,
providerFiles,
cache,
chatwootCache,
baileysCache,
);
@ -78,9 +60,6 @@ export const templateController = new TemplateController(templateService);
const proxyService = new ProxyService(waMonitor);
export const proxyController = new ProxyController(proxyService, waMonitor);
const chatwootService = new ChatwootService(waMonitor, configService, prismaRepository, chatwootCache);
export const chatwootController = new ChatwootController(chatwootService, configService, prismaRepository);
const settingsService = new SettingsService(waMonitor);
export const settingsController = new SettingsController(settingsService);
@ -89,11 +68,9 @@ export const instanceController = new InstanceController(
configService,
prismaRepository,
eventEmitter,
chatwootService,
settingsService,
proxyController,
cache,
chatwootCache,
baileysCache,
providerFiles,
);
@ -112,23 +89,8 @@ export const channelController = new ChannelController(prismaRepository, waMonit
export const metaController = new MetaController(prismaRepository, waMonitor);
export const baileysController = new BaileysController(waMonitor);
const openaiService = new OpenaiService(waMonitor, prismaRepository, configService);
export const openaiController = new OpenaiController(openaiService, prismaRepository, waMonitor);
// chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository, openaiService);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);
const difyService = new DifyService(waMonitor, prismaRepository, configService, openaiService);
export const difyController = new DifyController(difyService, prismaRepository, waMonitor);
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
const n8nService = new N8nService(waMonitor, prismaRepository, configService);
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService, openaiService);
export const evoaiController = new EvoaiController(evoaiService, prismaRepository, waMonitor);
logger.info('Module - ON');

View File

@ -1,15 +1,10 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { ProxyDto } from '@api/dto/proxy.dto';
import { SettingsDto } from '@api/dto/settings.dto';
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service';
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
import { PrismaRepository, Query } from '@api/repository/repository.service';
import { eventManager, waMonitor } from '@api/server.module';
import { eventManager } from '@api/server.module';
import { Events, wa } from '@api/types/wa.types';
import { Auth, Chatwoot, ConfigService, HttpServer, Proxy } from '@config/env.config';
import { Auth, ConfigService, HttpServer, Proxy } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import { Contact, Message, Prisma } from '@prisma/client';
@ -19,38 +14,21 @@ import { isArray } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import { v4 } from 'uuid';
import { CacheService } from './cache.service';
export class ChannelStartupService {
constructor(
public readonly configService: ConfigService,
public readonly eventEmitter: EventEmitter2,
public readonly prismaRepository: PrismaRepository,
public readonly chatwootCache: CacheService,
) {}
public readonly logger = new Logger('ChannelStartupService');
public client: WASocket;
public readonly instance: wa.Instance = {};
public readonly localChatwoot: wa.LocalChatwoot = {};
public readonly localProxy: wa.LocalProxy = {};
public readonly localSettings: wa.LocalSettings = {};
public readonly localWebhook: wa.LocalWebHook = {};
public chatwootService = new ChatwootService(
waMonitor,
this.configService,
this.prismaRepository,
this.chatwootCache,
);
public openaiService = new OpenaiService(waMonitor, this.prismaRepository, this.configService);
public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository, this.openaiService);
public difyService = new DifyService(waMonitor, this.prismaRepository, this.configService, this.openaiService);
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceName);
@ -60,17 +38,6 @@ export class ChannelStartupService {
this.instance.number = instance.number;
this.instance.token = instance.token;
this.instance.businessId = instance.businessId;
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
Events.STATUS_INSTANCE,
{ instanceName: this.instance.name },
{
instance: this.instance.name,
status: 'created',
},
);
}
}
public set instanceName(name: string) {
@ -221,146 +188,6 @@ export class ChannelStartupService {
};
}
public async loadChatwoot() {
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
return;
}
const data = await this.prismaRepository.chatwoot.findUnique({
where: {
instanceId: this.instanceId,
},
});
this.localChatwoot.enabled = data?.enabled;
this.localChatwoot.accountId = data?.accountId;
this.localChatwoot.token = data?.token;
this.localChatwoot.url = data?.url;
this.localChatwoot.nameInbox = data?.nameInbox;
this.localChatwoot.signMsg = data?.signMsg;
this.localChatwoot.signDelimiter = data?.signDelimiter;
this.localChatwoot.number = data?.number;
this.localChatwoot.reopenConversation = data?.reopenConversation;
this.localChatwoot.conversationPending = data?.conversationPending;
this.localChatwoot.mergeBrazilContacts = data?.mergeBrazilContacts;
this.localChatwoot.importContacts = data?.importContacts;
this.localChatwoot.importMessages = data?.importMessages;
this.localChatwoot.daysLimitImportMessages = data?.daysLimitImportMessages;
}
public async setChatwoot(data: ChatwootDto) {
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
return;
}
const chatwoot = await this.prismaRepository.chatwoot.findUnique({
where: {
instanceId: this.instanceId,
},
});
if (chatwoot) {
await this.prismaRepository.chatwoot.update({
where: {
instanceId: this.instanceId,
},
data: {
enabled: data?.enabled,
accountId: data.accountId,
token: data.token,
url: data.url,
nameInbox: data.nameInbox,
signMsg: data.signMsg,
signDelimiter: data.signMsg ? data.signDelimiter : null,
number: data.number,
reopenConversation: data.reopenConversation,
conversationPending: data.conversationPending,
mergeBrazilContacts: data.mergeBrazilContacts,
importContacts: data.importContacts,
importMessages: data.importMessages,
daysLimitImportMessages: data.daysLimitImportMessages,
organization: data.organization,
logo: data.logo,
ignoreJids: data.ignoreJids,
},
});
Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null });
this.clearCacheChatwoot();
return;
}
await this.prismaRepository.chatwoot.create({
data: {
enabled: data?.enabled,
accountId: data.accountId,
token: data.token,
url: data.url,
nameInbox: data.nameInbox,
signMsg: data.signMsg,
number: data.number,
reopenConversation: data.reopenConversation,
conversationPending: data.conversationPending,
mergeBrazilContacts: data.mergeBrazilContacts,
importContacts: data.importContacts,
importMessages: data.importMessages,
daysLimitImportMessages: data.daysLimitImportMessages,
organization: data.organization,
logo: data.logo,
ignoreJids: data.ignoreJids,
instanceId: this.instanceId,
},
});
Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null });
this.clearCacheChatwoot();
}
public async findChatwoot(): Promise<ChatwootDto | null> {
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
return null;
}
const data = await this.prismaRepository.chatwoot.findUnique({
where: {
instanceId: this.instanceId,
},
});
if (!data) {
return null;
}
const ignoreJidsArray = Array.isArray(data.ignoreJids) ? data.ignoreJids.map((event) => String(event)) : [];
return {
enabled: data?.enabled,
accountId: data.accountId,
token: data.token,
url: data.url,
nameInbox: data.nameInbox,
signMsg: data.signMsg,
signDelimiter: data.signDelimiter || null,
reopenConversation: data.reopenConversation,
conversationPending: data.conversationPending,
mergeBrazilContacts: data.mergeBrazilContacts,
importContacts: data.importContacts,
importMessages: data.importMessages,
daysLimitImportMessages: data.daysLimitImportMessages,
organization: data.organization,
logo: data.logo,
ignoreJids: ignoreJidsArray,
};
}
public clearCacheChatwoot() {
if (this.localChatwoot?.enabled) {
this.chatwootService.getCache()?.deleteAll(this.instanceName);
}
}
public async loadProxy() {
this.localProxy.enabled = false;

View File

@ -3,7 +3,7 @@ import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { channelController } from '@api/server.module';
import { Events, Integration } from '@api/types/wa.types';
import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
import { CacheConf, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions';
@ -21,7 +21,6 @@ export class WAMonitoringService {
private readonly prismaRepository: PrismaRepository,
private readonly providerFiles: ProviderFiles,
private readonly cache: CacheService,
private readonly chatwootCache: CacheService,
private readonly baileysCache: CacheService,
) {
this.removeInstance();
@ -90,7 +89,6 @@ export class WAMonitoringService {
const instances = await this.prismaRepository.instance.findMany({
where,
include: {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
@ -170,11 +168,6 @@ export class WAMonitoringService {
}
public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
execFileSync('rm', ['-rf', instancePath]);
}
const instance = await this.prismaRepository.instance.findFirst({
where: { name: instanceName },
});
@ -191,13 +184,11 @@ export class WAMonitoringService {
await this.prismaRepository.message.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.webhook.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.websocket.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.setting.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.label.deleteMany({ where: { instanceId: instance.id } });
@ -257,7 +248,6 @@ export class WAMonitoringService {
eventEmitter: this.eventEmitter,
prismaRepository: this.prismaRepository,
cache: this.cache,
chatwootCache: this.chatwootCache,
baileysCache: this.baileysCache,
providerFiles: this.providerFiles,
});
@ -377,10 +367,6 @@ export class WAMonitoringService {
try {
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
this.waInstances[instanceName]?.clearCacheChatwoot();
}
this.cleaningUp(instanceName);
} finally {
this.logger.warn(`Instance "${instanceName}" - LOGOUT`);

View File

@ -28,8 +28,6 @@ export enum Events {
GROUPS_UPDATE = 'groups.update',
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
CALL = 'call',
TYPEBOT_START = 'typebot.start',
TYPEBOT_CHANGE_STATUS = 'typebot.change-status',
LABELS_EDIT = 'labels.edit',
LABELS_ASSOCIATION = 'labels.association',
CREDS_UPDATE = 'creds.update',
@ -61,23 +59,6 @@ export declare namespace wa {
businessId?: string;
};
export type LocalChatwoot = {
enabled?: boolean;
accountId?: string;
token?: string;
url?: string;
nameInbox?: string;
signMsg?: boolean;
signDelimiter?: string;
number?: string;
reopenConversation?: boolean;
conversationPending?: boolean;
mergeBrazilContacts?: boolean;
importContacts?: boolean;
importMessages?: boolean;
daysLimitImportMessages?: number;
};
export type LocalSettings = {
rejectCall?: boolean;
msgCall?: string;

View File

@ -84,8 +84,6 @@ export const instanceSchema: JSONSchema7 = {
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
@ -121,8 +119,6 @@ export const instanceSchema: JSONSchema7 = {
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
@ -158,8 +154,6 @@ export const instanceSchema: JSONSchema7 = {
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
@ -195,23 +189,9 @@ export const instanceSchema: JSONSchema7 = {
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
// Chatwoot
chatwootAccountId: { type: 'string' },
chatwootToken: { type: 'string' },
chatwootUrl: { type: 'string' },
chatwootSignMsg: { type: 'boolean' },
chatwootReopenConversation: { type: 'boolean' },
chatwootConversationPending: { type: 'boolean' },
chatwootImportContacts: { type: 'boolean' },
chatwootNameInbox: { type: 'string' },
chatwootMergeBrazilContacts: { type: 'boolean' },
chatwootImportMessages: { type: 'boolean' },
chatwootDaysLimitImportMessages: { type: 'number' },
},
...isNotEmpty('instanceName'),
};