Add N8n integration with models, services, and routes

- Introduced N8n and N8nSetting models in both MySQL and PostgreSQL schemas.
- Implemented N8nController and N8nService for managing N8n bots.
- Created N8nRouter for handling API requests related to N8n.
- Added DTOs and validation schemas for N8n integration.
- Updated server module and chatbot controller to include N8n functionality.
- Configured environment settings for N8n integration.
This commit is contained in:
Guilherme Gomes
2025-05-14 20:47:23 -03:00
parent 6d63f2fb6e
commit 38f089f04c
14 changed files with 1708 additions and 5 deletions

View File

@@ -134,7 +134,6 @@ import { randomBytes } from 'crypto';
import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg';
import FormData from 'form-data';
import { readFileSync } from 'fs';
import Long from 'long';
import mimeTypes from 'mime-types';
import NodeCache from 'node-cache';
@@ -230,10 +229,10 @@ export class BaileysStartupService extends ChannelStartupService {
private authStateProvider: AuthStateProvider;
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache({
stdTTL: 300000,
useClones: false
});
private readonly userDevicesCache: CacheStore = new NodeCache({
stdTTL: 300000,
useClones: false,
});
private endSession = false;
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;

View File

@@ -4,6 +4,7 @@ import {
difyController,
evolutionBotController,
flowiseController,
n8nController,
openaiController,
typebotController,
} from '@api/server.module';
@@ -97,6 +98,8 @@ export class ChatbotController {
await difyController.emit(emitData);
await n8nController.emit(emitData);
await flowiseController.emit(emitData);
}

View File

@@ -6,6 +6,7 @@ import { Router } from 'express';
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
import { FlowiseRouter } from './flowise/routes/flowise.router';
import { N8nRouter } from './n8n/routes/n8n.router';
export class ChatbotRouter {
public readonly router: Router;
@@ -19,5 +20,6 @@ export class ChatbotRouter {
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);
}
}

View File

@@ -4,3 +4,4 @@ export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.sche
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';

View File

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

View File

@@ -0,0 +1,34 @@
import { TriggerType, TriggerOperator } from '@prisma/client';
export class N8nDto {
enabled?: boolean;
description?: string;
webhookUrl?: string;
basicAuthUser?: string;
basicAuthPass?: string;
// Advanced bot properties (copied from DifyDto style)
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
}
export class N8nSettingDto {
// Add settings fields here if needed for compatibility
}
export class N8nMessageDto {
chatInput: string;
sessionId: string;
}

View File

@@ -0,0 +1,113 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { N8nDto, N8nSettingDto } from '../dto/n8n.dto';
import { HttpStatus } from '@api/routes/index.router';
import { n8nController } from '@api/server.module';
import {
n8nIgnoreJidSchema,
n8nSchema,
n8nSettingSchema,
n8nStatusSchema,
instanceSchema,
} from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class N8nRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nDto>({
request: req,
schema: n8nSchema,
ClassRef: N8nDto,
execute: (instance, data) => n8nController.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) => n8nController.findBot(instance),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetch/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.fetchBot(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('update/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nDto>({
request: req,
schema: n8nSchema,
ClassRef: N8nDto,
execute: (instance, data) => n8nController.updateBot(instance, req.params.n8nId, data),
});
res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.deleteBot(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('settings'), ...guards, async (req, res) => {
const response = await this.dataValidate<N8nSettingDto>({
request: req,
schema: n8nSettingSchema,
ClassRef: N8nSettingDto,
execute: (instance, data) => n8nController.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) => n8nController.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: n8nStatusSchema,
ClassRef: InstanceDto,
execute: (instance, data) => n8nController.changeStatus(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchSessions/:n8nId'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => n8nController.fetchSessions(instance, req.params.n8nId),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
const response = await this.dataValidate<IgnoreJidDto>({
request: req,
schema: n8nIgnoreJidSchema,
ClassRef: IgnoreJidDto,
execute: (instance, data) => n8nController.ignoreJid(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -0,0 +1,431 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { Logger } from '@config/logger.config';
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { InstanceDto } from '@api/dto/instance.dto';
import { N8nDto } from '../dto/n8n.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class N8nService {
private readonly logger = new Logger('N8nService');
private readonly waMonitor: WAMonitoringService;
constructor(
waMonitor: WAMonitoringService,
private readonly prismaRepository: PrismaRepository
) {
this.waMonitor = waMonitor;
}
/**
* Create a new N8n bot for the given instance.
*/
public async createBot(instanceId: string, data: N8nDto) {
try {
return await this.prismaRepository.n8n.create({
data: {
enabled: data.enabled ?? true,
description: data.description,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
instanceId,
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Find all N8n bots for the given instance.
*/
public async findBots(instanceId: string) {
try {
return await this.prismaRepository.n8n.findMany({ where: { instanceId } });
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Fetch a specific N8n bot by ID and instance.
*/
public async fetchBot(instanceId: string, n8nId: string) {
try {
const bot = await this.prismaRepository.n8n.findFirst({ where: { id: n8nId } });
if (!bot || bot.instanceId !== instanceId) throw new Error('N8n bot not found');
return bot;
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Update a specific N8n bot.
*/
public async updateBot(instanceId: string, n8nId: string, data: N8nDto) {
try {
await this.fetchBot(instanceId, n8nId);
return await this.prismaRepository.n8n.update({
where: { id: n8nId },
data: {
enabled: data.enabled,
description: data.description,
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Delete a specific N8n bot.
*/
public async deleteBot(instanceId: string, n8nId: string) {
try {
await this.fetchBot(instanceId, n8nId);
return await this.prismaRepository.n8n.delete({ where: { id: n8nId } });
} catch (error) {
this.logger.error(error);
throw error;
}
}
/**
* Send a message to the N8n bot webhook.
*/
public async sendMessage(n8nId: string, chatInput: string, sessionId: string): Promise<string> {
try {
const bot = await this.prismaRepository.n8n.findFirst({ where: { id: n8nId, enabled: true } });
if (!bot) throw new Error('N8n bot not found or not enabled');
const headers: Record<string, string> = {};
if (bot.basicAuthUser && bot.basicAuthPass) {
const auth = Buffer.from(`${bot.basicAuthUser}:${bot.basicAuthPass}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
}
const response = await axios.post(bot.webhookUrl, { chatInput, sessionId }, { headers });
return response.data.output;
} catch (error) {
this.logger.error(error);
throw new Error('Error sending message to n8n bot');
}
}
public async createNewSession(instance: InstanceDto, data: any) {
try {
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: data.remoteJid,
pushName: data.pushName,
sessionId: data.remoteJid,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: 'n8n',
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
}
private isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
private async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: N8nSetting,
n8n: N8n,
remoteJid: string,
pushName: string,
content: string,
) {
try {
let endpoint: string = n8n.webhookUrl;
const payload: any = {
chatInput: content,
sessionId: session.sessionId,
};
const headers: Record<string, string> = {};
if (n8n.basicAuthUser && n8n.basicAuthPass) {
const auth = Buffer.from(`${n8n.basicAuthUser}:${n8n.basicAuthPass}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
}
const response = await axios.post(endpoint, payload, { headers });
const message = response?.data?.output || response?.data?.answer;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
} catch (error) {
this.logger.error(error.response?.data || error);
return;
}
}
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: N8nSetting) {
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const getMediaType = (url: string): string | null => {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
};
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclMark, altText, url] = match;
const mediaType = getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
});
} else {
await instance.mediaMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: mediaType,
media: url,
caption: altText,
},
null,
false,
);
}
} else {
textBuffer += `[${altText}](${url})`;
}
lastIndex = linkRegex.lastIndex;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
if (splitMessages) {
const multipleMessages = textBuffer.trim().split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
}
}
sendTelemetry('/message/sendText');
}
private async initNewSession(
instance: any,
remoteJid: string,
n8n: N8n,
settings: N8nSetting,
session: IntegrationSession,
content: string,
pushName?: string,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: n8n.id,
});
if (data.session) {
session = data.session;
}
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName, content);
return;
}
public async processN8n(
instance: any,
remoteJid: string,
n8n: N8n,
session: IntegrationSession,
settings: N8nSetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
return;
}
if (session && settings.expire && settings.expire > 0) {
const now = Date.now();
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
const diff = now - sessionUpdatedAt;
const diffInMinutes = Math.floor(diff / 1000 / 60);
if (diffInMinutes > settings.expire) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: { id: session.id },
data: { status: 'closed' },
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: { botId: n8n.id, remoteJid: remoteJid },
});
}
await this.initNewSession(instance, remoteJid, n8n, settings, session, content, pushName);
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, n8n, settings, session, content, pushName);
return;
}
await this.prismaRepository.integrationSession.update({
where: { id: session.id },
data: { status: 'opened', awaitUser: false },
});
if (!content) {
if (settings.unknownMessage) {
this.waMonitor.waInstances[instance.instanceName].textMessage(
{
number: remoteJid.split('@')[0],
delay: settings.delayMessage || 1000,
text: settings.unknownMessage,
},
false,
);
sendTelemetry('/message/sendText');
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: { id: session.id },
data: { status: 'closed' },
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: { botId: n8n.id, remoteJid: remoteJid },
});
}
return;
}
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName, content);
return;
}
}

View File

@@ -0,0 +1,59 @@
import { JSONSchema7 } from 'json-schema';
export const n8nSchema: JSONSchema7 = {
type: 'object',
properties: {
enabled: { type: 'boolean' },
description: { type: 'string' },
webhookUrl: { type: 'string', minLength: 1 },
basicAuthUser: { type: 'string' },
basicAuthPass: { type: 'string' },
},
required: ['enabled', 'webhookUrl'],
};
export const n8nMessageSchema: JSONSchema7 = {
type: 'object',
properties: {
chatInput: { type: 'string', minLength: 1 },
sessionId: { type: 'string', minLength: 1 },
},
required: ['chatInput', 'sessionId'],
};
export const n8nSettingSchema: JSONSchema7 = {
type: 'object',
properties: {
expire: { type: 'number' },
keywordFinish: { type: 'string' },
delayMessage: { type: 'number' },
unknownMessage: { type: 'string' },
listeningFromMe: { type: 'boolean' },
stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' },
debounceTime: { type: 'number' },
n8nIdFallback: { type: 'string' },
ignoreJids: { type: 'array', items: { type: 'string' } },
splitMessages: { type: 'boolean' },
timePerChar: { type: 'number' },
},
required: [],
};
export const n8nStatusSchema: JSONSchema7 = {
type: 'object',
properties: {
remoteJid: { type: 'string' },
status: { type: 'string', enum: ['opened', 'closed', 'delete', 'paused'] },
},
required: ['remoteJid', 'status'],
};
export const n8nIgnoreJidSchema: JSONSchema7 = {
type: 'object',
properties: {
remoteJid: { type: 'string' },
action: { type: 'string', enum: ['add', 'remove'] },
},
required: ['remoteJid', 'action'],
};