feat(chatbot): implement base chatbot structure and enhance integration capabilities

- Introduced a base structure for chatbot integrations, including BaseChatbotController and BaseChatbotService.
- Added common DTOs for chatbot settings and data to streamline integration processes.
- Updated existing chatbot controllers (Dify, Evoai, N8n) to extend from the new base classes, improving code reusability and maintainability.
- Enhanced media message handling across integrations, including audio transcription capabilities using OpenAI's Whisper API.
- Refactored service methods to accommodate new message structures and improve error handling.
This commit is contained in:
Guilherme Gomes 2025-05-17 16:22:13 -03:00
parent d3ee370bdc
commit 69b4f1aa02
13 changed files with 2152 additions and 3229 deletions

View File

@ -0,0 +1,921 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import { TriggerOperator, TriggerType } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { BaseChatbotDto } from './base-chatbot.dto';
import { ChatbotController, ChatbotControllerInterface, EmitData } from './chatbot.controller';
// Common settings interface for all chatbot integrations
export interface ChatbotSettings {
expire: number;
keywordFinish: string[];
delayMessage: number;
unknownMessage: string;
listeningFromMe: boolean;
stopBotFromMe: boolean;
keepOpen: boolean;
debounceTime: number;
ignoreJids: string[];
splitMessages: boolean;
timePerChar: number;
[key: string]: any;
}
// Common bot properties for all chatbot integrations
export interface BaseBotData {
enabled?: boolean;
description: string;
expire?: number;
keywordFinish?: string[];
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: string | TriggerType;
triggerOperator?: string | TriggerOperator;
triggerValue?: string;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
[key: string]: any;
}
export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto> extends ChatbotController implements ChatbotControllerInterface {
public readonly logger: Logger;
integrationEnabled: boolean;
botRepository: any;
settingsRepository: any;
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
// Name of the integration, to be set by the derived class
protected abstract readonly integrationName: string;
// Method to process bot-specific logic
protected abstract processBot(
waInstance: any,
remoteJid: string,
bot: BotType,
session: any,
settings: ChatbotSettings,
content: string,
pushName?: string,
msg?: any,
): Promise<void>;
// Method to get the fallback bot ID from settings
protected abstract getFallbackBotId(settings: any): string | undefined;
constructor(
prismaRepository: PrismaRepository,
waMonitor: WAMonitoringService,
) {
super(prismaRepository, waMonitor);
this.sessionRepository = this.prismaRepository.integrationSession;
}
// Base create bot implementation
public async createBot(instance: InstanceDto, data: BotData) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
// Set default settings if not provided
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 a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`);
}
// Check for trigger keyword duplicates
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');
}
}
// Check for trigger advanced duplicates
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');
}
}
// Derived classes should implement the specific duplicate checking before calling this method
// and add bot-specific fields to the data object
try {
const botData = {
enabled: data?.enabled,
description: data.description,
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,
...this.getAdditionalBotData(data),
};
const bot = await this.botRepository.create({
data: botData,
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error creating ${this.integrationName}`);
}
}
// Additional fields needed for specific bot types
protected abstract getAdditionalBotData(data: BotData): Record<string, any>;
// Common implementation for findBot
public async findBot(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
try {
const bots = await this.botRepository.findMany({
where: {
instanceId: instanceId,
},
});
return bots;
} catch (error) {
this.logger.error(error);
throw new Error(`Error finding ${this.integrationName}`);
}
}
// Common implementation for fetchBot
public async fetchBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
try {
const bot = await this.botRepository.findUnique({
where: {
id: botId,
},
});
if (!bot) {
throw new Error(`${this.integrationName} not found`);
}
return bot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error fetching ${this.integrationName}`);
}
}
// Common implementation for settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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,
},
});
// Get the name of the fallback field for this integration type
const fallbackFieldName = this.getFallbackFieldName();
const settingsData = {
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,
[fallbackFieldName]: data.fallbackId, // Use the correct field name dynamically
};
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[fallbackFieldName]
};
} else {
const settings = await this.settingsRepository.create({
data: {
...settingsData,
instanceId: instanceId,
},
});
// Map the specific fallback field to a generic 'fallbackId' in the response
return {
...settings,
fallbackId: settings[fallbackFieldName]
};
}
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Abstract method to get the field name for the fallback ID
protected abstract getFallbackFieldName(): string;
// Abstract method to get the integration type (dify, n8n, evoai, etc.)
protected abstract getIntegrationType(): string;
// Common implementation for fetchSettings
public async fetchSettings(instance: InstanceDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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,
},
});
// Get the name of the fallback field for this integration type
const fallbackFieldName = this.getFallbackFieldName();
if (!settings) {
return {
expire: 300,
keywordFinish: ['bye', 'exit', 'quit', 'stop'],
delayMessage: 1000,
unknownMessage: 'Sorry, I dont understand',
listeningFromMe: true,
stopBotFromMe: true,
keepOpen: false,
debounceTime: 1,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
fallbackId: '',
fallback: null,
};
}
// Return with standardized fallbackId field
return {
...settings,
fallbackId: settings[fallbackFieldName],
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching settings');
}
}
// Common implementation for changeStatus
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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 ${this.integrationName} status`);
}
}
// Common implementation for fetchSessions
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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(`${this.integrationName} not found`);
}
// Get the integration type (dify, n8n, evoai, etc.)
const integrationType = this.getIntegrationType();
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: integrationType,
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
// Common implementation for ignoreJid
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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');
}
}
// Base implementation for updateBot
public async updateBot(instance: InstanceDto, botId: string, data: BotData) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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) {
throw new Error(`${this.integrationName} not found`);
}
if (bot.instanceId !== instanceId) {
throw new Error(`${this.integrationName} not found`);
}
// Check for "all" trigger type conflicts
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error(`You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`);
}
}
// Let subclasses check for integration-specific duplicates
await this.validateNoDuplicatesOnUpdate(botId, instanceId, data);
// Check for keyword trigger duplicates
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');
}
}
// Check for advanced trigger duplicates
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');
}
}
// Combine common fields with bot-specific fields
const updateData = {
enabled: data?.enabled,
description: data.description,
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,
...this.getAdditionalUpdateFields(data),
};
const updatedBot = await this.botRepository.update({
where: {
id: botId,
},
data: updateData,
});
return updatedBot;
} catch (error) {
this.logger.error(error);
throw new Error(`Error updating ${this.integrationName}`);
}
}
// Abstract method for validating bot-specific duplicates on update
protected abstract validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: BotData): Promise<void>;
// Abstract method for getting additional fields for update
protected abstract getAdditionalUpdateFields(data: BotData): Record<string, any>;
// Base implementation for deleteBot
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} 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) {
throw new Error(`${this.integrationName} not found`);
}
if (bot.instanceId !== instanceId) {
throw new Error(`${this.integrationName} not found`);
}
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 ${this.integrationName} bot`);
}
}
// Base implementation for 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);
// Get integration type
const integrationType = this.getIntegrationType();
// Find a bot for this message
let findBot: any = await this.findBotTrigger(this.botRepository, content, instance, session);
// If no bot is found, try to use fallback
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
// Get the fallback ID for this integration type
const fallbackId = this.getFallbackBotId(fallback);
if (fallbackId) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallbackId,
},
});
findBot = findFallback;
} else {
return;
}
}
// If we still don't have a bot, return
if (!findBot) {
return;
}
// Collect settings with fallbacks to default settings
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;
};
// Handle stopping the bot if message is from me
if (stopBotFromMe && key.fromMe && session) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'paused',
},
});
return;
}
// Skip if not listening to messages from me
if (!listeningFromMe && key.fromMe) {
return;
}
// Skip if session exists but not awaiting user input
if (session && !session.awaitUser) {
return;
}
// Merged settings
const mergedSettings = {
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
};
// Process with debounce if needed
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
mergedSettings,
debouncedContent,
msg?.pushName,
msg,
);
});
} else {
await this.processBot(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
mergedSettings,
content,
msg?.pushName,
msg,
);
}
} catch (error) {
this.logger.error(error);
}
}
}

View File

@ -0,0 +1,42 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
/**
* Base DTO for all chatbot integrations
* Contains common properties shared by all chatbot types
*/
export class BaseChatbotDto {
enabled?: boolean;
description: string;
expire?: number;
keywordFinish?: string[];
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: string[];
splitMessages?: boolean;
timePerChar?: number;
}
/**
* Base settings DTO for all chatbot integrations
*/
export class BaseChatbotSettingDto {
expire?: number;
keywordFinish?: string[];
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
fallbackId?: string; // Unified fallback ID field for all integrations
}

View File

@ -0,0 +1,443 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { ConfigService, Language } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';
import { Integration } from '@api/types/wa.types';
import axios from 'axios';
import FormData from 'form-data';
/**
* Base class for all chatbot service implementations
* Contains common methods shared across different chatbot integrations
*/
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
protected readonly logger: Logger;
protected readonly waMonitor: WAMonitoringService;
protected readonly prismaRepository: PrismaRepository;
protected readonly configService?: ConfigService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
loggerName: string,
configService?: ConfigService,
) {
this.waMonitor = waMonitor;
this.prismaRepository = prismaRepository;
this.logger = new Logger(loggerName);
this.configService = configService;
}
/**
* Check if a message contains an image
*/
protected isImageMessage(content: string): boolean {
return content.includes('imageMessage');
}
/**
* Check if a message contains audio
*/
protected isAudioMessage(content: string): boolean {
return content.includes('audioMessage');
}
/**
* Check if a string is valid JSON
*/
protected isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
/**
* Determine the media type from a URL based on its extension
*/
protected 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;
}
/**
* Transcribes audio to text using OpenAI's Whisper API
*/
protected async speechToText(audioBuffer: Buffer): Promise<string | null> {
if (!this.configService) {
this.logger.error('ConfigService not available for speech-to-text transcription');
return null;
}
try {
// Try to get the API key from process.env directly since ConfigService might not access it correctly
const apiKey = this.configService.get<any>('OPENAI')?.API_KEY || process.env.OPENAI_API_KEY;
if (!apiKey) {
this.logger.error('No OpenAI API key set for Whisper transcription');
return null;
}
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
? 'pt'
: this.configService.get<Language>('LANGUAGE');
const formData = new FormData();
formData.append('file', audioBuffer, 'audio.ogg');
formData.append('model', 'whisper-1');
formData.append('language', lang);
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${apiKey}`,
},
});
return response?.data?.text || null;
} catch (err) {
this.logger.error(`Whisper transcription failed: ${err}`);
return null;
}
}
/**
* Create a new chatbot session
*/
public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
try {
// Extract pushName safely - if data.pushName is an object with a pushName property, use that
const pushNameValue = typeof data.pushName === 'object' && data.pushName?.pushName
? data.pushName.pushName
: (typeof data.pushName === 'string' ? data.pushName : null);
// Extract remoteJid safely
const remoteJidValue = typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid
? data.remoteJid.remoteJid
: data.remoteJid;
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: remoteJidValue,
pushName: pushNameValue,
sessionId: remoteJidValue,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: type,
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
/**
* Standard implementation for processing incoming messages
* This handles the common workflow across all chatbot types:
* 1. Check for existing session or create new one
* 2. Handle message based on session state
*/
public async process(
instance: any,
remoteJid: string,
bot: BotType,
session: IntegrationSession,
settings: SettingsType,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
try {
// For new sessions or sessions awaiting initialization
if (!session || session.status === 'paused') {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
return;
}
// For existing sessions, keywords might indicate the conversation should end
const keywordFinish = (settings as any)?.keywordFinish || [];
const normalizedContent = content.toLowerCase().trim();
if (
keywordFinish.length > 0 &&
keywordFinish.some((keyword: string) => normalizedContent === keyword.toLowerCase())
) {
// Update session to closed and return
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
return;
}
// Forward the message to the chatbot API
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(`Error in process: ${error}`);
return;
}
}
/**
* Standard implementation for sending messages to WhatsApp
* This handles common patterns like markdown links and formatting
*/
protected async sendMessageWhatsApp(
instance: any,
remoteJid: string,
message: string,
settings: SettingsType
): Promise<void> {
if (!message) return;
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const splitMessages = (settings as any)?.splitMessages ?? false;
const timePerChar = (settings as any)?.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, exclamation, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
// Send accumulated text before sending media
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
textBuffer = '';
}
// Handle sending the media
try {
switch (mediaType) {
case 'image':
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
caption: altText,
media: url,
});
break;
case 'video':
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
caption: altText,
media: url,
});
break;
case 'document':
await instance.documentMessage({
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
fileName: altText || 'document',
media: url,
});
break;
case 'audio':
await instance.audioMessage({
number: remoteJid.split('@')[0],
delay: (settings as any)?.delayMessage || 1000,
media: url,
});
break;
}
} catch (error) {
this.logger.error(`Error sending media: ${error}`);
// If media fails, at least send the alt text and URL
textBuffer += `${altText}: ${url}`;
}
} else {
// It's a regular link, keep it in the text
textBuffer += `[${altText}](${url})`;
}
lastIndex = match.index + fullMatch.length;
}
// Add any remaining text after the last match
if (lastIndex < message.length) {
textBuffer += message.slice(lastIndex);
}
// Send any remaining text
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
}
}
/**
* Helper method to send formatted text with proper typing indicators and delays
*/
private async sendFormattedText(
instance: any,
remoteJid: string,
text: string,
settings: any,
splitMessages: boolean
): Promise<void> {
const timePerChar = settings?.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const multipleMessages = text.split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
if (!message.trim()) continue;
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 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 === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 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: text,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
/**
* Standard implementation for initializing a new session
* This method should be overridden if a subclass needs specific initialization
*/
protected async initNewSession(
instance: any,
remoteJid: string,
bot: BotType,
settings: SettingsType,
session: IntegrationSession,
content: string,
pushName?: string | any,
msg?: any,
): Promise<void> {
// Create a session if none exists
if (!session) {
// Extract pushName properly - if it's an object with pushName property, use that
const pushNameValue = typeof pushName === 'object' && pushName?.pushName
? pushName.pushName
: (typeof pushName === 'string' ? pushName : null);
session = (
await this.createNewSession(
{
instanceName: instance.instanceName,
instanceId: instance.instanceId
},
{
remoteJid,
pushName: pushNameValue,
botId: (bot as any).id,
},
this.getBotType(),
)
)?.session;
}
// Update session status to opened
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
// Forward the message to the chatbot
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
}
/**
* Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
* This should match the type field used in the IntegrationSession
*/
protected abstract getBotType(): string;
/**
* Send a message to the chatbot API
* This is specific to each chatbot integration
*/
protected abstract sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: SettingsType,
bot: BotType,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void>;
}

View File

@ -1,4 +1,4 @@
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
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';
@ -7,12 +7,11 @@ 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 } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class DifyController extends ChatbotController implements ChatbotControllerInterface {
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
constructor(
private readonly difyService: DifyService,
prismaRepository: PrismaRepository,
@ -26,6 +25,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
}
public readonly logger = new Logger('DifyController');
protected readonly integrationName = 'Dify';
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
botRepository: any;
@ -33,6 +33,54 @@ export class DifyController extends ChatbotController implements ChatbotControll
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');
}
}
// Bots
public async createBot(instance: InstanceDto, data: DifyDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
@ -45,74 +93,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
})
.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 a dify with an "All" trigger, you cannot have more bots while it is active');
}
// Check for Dify-specific duplicate
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -126,72 +107,8 @@ export class DifyController extends ChatbotController implements ChatbotControll
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.create({
data: {
enabled: data?.enabled,
description: data.description,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating dify');
}
// Let the base class handle the rest of the bot creation process
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
@ -246,643 +163,17 @@ export class DifyController extends ChatbotController implements ChatbotControll
return bot;
}
public async updateBot(instance: InstanceDto, botId: string, 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);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
}
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
id: {
not: botId,
},
instanceId: instanceId,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Dify already exists');
}
if (data.triggerType === 'keyword') {
if (!data.triggerOperator || !data.triggerValue) {
throw new Error('Trigger operator and value are required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
if (data.triggerType === 'advanced') {
if (!data.triggerValue) {
throw new Error('Trigger value is required');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
triggerValue: data.triggerValue,
id: { not: botId },
instanceId: instanceId,
},
});
if (checkDuplicate) {
throw new Error('Trigger already exists');
}
}
try {
const bot = await this.botRepository.update({
where: {
id: botId,
},
data: {
enabled: data?.enabled,
description: data.description,
botType: data.botType,
apiUrl: data.apiUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating dify');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Dify not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
try {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: botId,
},
});
await this.botRepository.delete({
where: {
id: botId,
},
});
return { bot: { id: botId } };
} catch (error) {
this.logger.error(error);
throw new Error('Error deleting dify bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (settings) {
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
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,
difyIdFallback: updateSettings.difyIdFallback,
ignoreJids: updateSettings.ignoreJids,
splitMessages: updateSettings.splitMessages,
timePerChar: updateSettings.timePerChar,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
difyIdFallback: data.difyIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
difyIdFallback: newSetttings.difyIdFallback,
ignoreJids: newSetttings.ignoreJids,
splitMessages: newSetttings.splitMessages,
timePerChar: newSetttings.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('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
include: {
Fallback: true,
},
});
if (!settings) {
return {
expire: 0,
keywordFinish: '',
delayMessage: 0,
unknownMessage: '',
listeningFromMe: false,
stopBotFromMe: false,
keepOpen: false,
ignoreJids: [],
splitMessages: false,
timePerChar: 0,
difyIdFallback: '',
fallback: null,
};
}
return {
expire: settings.expire,
keywordFinish: settings.keywordFinish,
delayMessage: settings.delayMessage,
unknownMessage: settings.unknownMessage,
listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen,
ignoreJids: settings.ignoreJids,
splitMessages: settings.splitMessages,
timePerChar: settings.timePerChar,
difyIdFallback: settings.difyIdFallback,
fallback: settings.Fallback,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching default settings');
}
}
// Sessions
public async changeStatus(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
instanceId,
},
});
const remoteJid = data.remoteJid;
const status = data.status;
if (status === 'delete') {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
return { bot: { remoteJid: remoteJid, status: status } };
}
if (status === 'closed') {
if (defaultSettingCheck?.keepOpen) {
await this.sessionRepository.updateMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: 'closed',
},
});
} else {
await this.sessionRepository.deleteMany({
where: {
remoteJid: remoteJid,
botId: { not: null },
},
});
}
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
} else {
const session = await this.sessionRepository.updateMany({
where: {
instanceId: instanceId,
remoteJid: remoteJid,
botId: { not: null },
},
data: {
status: status,
},
});
const botData = {
remoteJid: remoteJid,
status: status,
session,
};
return { bot: { ...instance, bot: botData } };
}
} catch (error) {
this.logger.error(error);
throw new Error('Error changing status');
}
}
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (bot && bot.instanceId !== instanceId) {
throw new Error('Dify not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: 'dify',
},
});
} catch (error) {
this.logger.error(error);
throw new Error('Error fetching sessions');
}
}
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
try {
const instanceId = await this.prismaRepository.instance
.findFirst({
where: {
name: instance.instanceName,
},
})
.then((instance) => instance.id);
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instanceId,
},
});
if (!settings) {
throw new Error('Settings not found');
}
let ignoreJids: any = settings?.ignoreJids || [];
if (data.action === 'add') {
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
ignoreJids.push(data.remoteJid);
} else {
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
}
const updateSettings = await this.settingsRepository.update({
where: {
id: settings.id,
},
data: {
ignoreJids: ignoreJids,
},
});
return {
ignoreJids: updateSettings.ignoreJids,
};
} catch (error) {
this.logger.error(error);
throw new Error('Error setting default settings');
}
}
// Emit
public async emit({ instance, remoteJid, msg }: EmitData) {
if (!this.integrationEnabled) return;
try {
const settings = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
const session = await this.getSession(remoteJid, instance);
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.difyIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.difyIdFallback,
},
});
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.difyService.processDify(
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.difyService.processDify(
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;
}
// Process Dify-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: DifyModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,38 +1,13 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class DifyDto {
enabled?: boolean;
description?: string;
export class DifyDto extends BaseChatbotDto {
// Dify specific fields
botType?: $Enums.DifyBotType;
apiUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class DifySettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
difyIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
export class DifySettingDto extends BaseChatbotSettingDto {
// Dify specific fields
}

View File

@ -1,60 +1,37 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { Auth, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { Readable } from 'stream';
import { downloadMediaMessage } from 'baileys';
export class DifyService {
import { BaseChatbotService } from '../../base-chatbot.service';
import { ConfigService, HttpServer } from '@config/env.config';
import { Auth } from '@config/env.config';
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
) {}
waMonitor: WAMonitoringService,
configService: ConfigService,
prismaRepository: PrismaRepository,
) {
super(waMonitor, prismaRepository, 'DifyService', configService);
}
private readonly logger = new Logger('DifyService');
/**
* Return the bot type for Dify
*/
protected getBotType(): string {
return 'dify';
}
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: 'dify',
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
return super.createNewSession(instance, data, 'dify');
}
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(
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: DifySetting,
@ -62,6 +39,7 @@ export class DifyService {
remoteJid: string,
pushName: string,
content: string,
msg?: any,
) {
try {
let endpoint: string = dify.apiUrl;
@ -82,6 +60,7 @@ export class DifyService {
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
@ -94,6 +73,23 @@ export class DifyService {
];
payload.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
const transcribedText = await this.speechToText(mediaBuffer);
if (transcribedText) {
payload.query = transcribedText;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
@ -142,6 +138,7 @@ export class DifyService {
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
@ -154,6 +151,23 @@ export class DifyService {
];
payload.inputs.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
const transcribedText = await this.speechToText(mediaBuffer);
if (transcribedText) {
payload.inputs.query = transcribedText;
} else {
payload.inputs.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.inputs.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
@ -202,6 +216,7 @@ export class DifyService {
user: remoteJid,
};
// Handle image messages
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
@ -214,6 +229,23 @@ export class DifyService {
];
payload.query = contentSplit[2] || content;
}
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
const transcribedText = await this.speechToText(mediaBuffer);
if (transcribedText) {
payload.query = transcribedText;
} else {
payload.query = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
payload.query = '[Audio message could not be transcribed]';
}
}
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
@ -248,9 +280,7 @@ export class DifyService {
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = answer;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
await this.prismaRepository.integrationSession.update({
where: {
@ -259,70 +289,9 @@ export class DifyService {
data: {
status: 'opened',
awaitUser: true,
sessionId: conversationId,
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
},
});
return;
}
if (dify.botType === 'workflow') {
endpoint += '/workflows/run';
const payload: any = {
inputs: {
query: content,
remoteJid: remoteJid,
pushName: pushName,
instanceName: instance.instanceName,
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
},
response_mode: 'blocking',
user: remoteJid,
};
if (this.isImageMessage(content)) {
const contentSplit = content.split('|');
payload.files = [
{
type: 'image',
transfer_method: 'remote_url',
url: contentSplit[1].split('?')[0],
},
];
payload.inputs.query = contentSplit[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?.data.outputs.text;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
return;
}
} catch (error) {
this.logger.error(error.response?.data || error);
@ -330,33 +299,17 @@ export class DifyService {
}
}
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
protected async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
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 [fullMatch, exclamation, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
@ -370,10 +323,8 @@ export class DifyService {
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 === Integration.WHATSAPP_BAILEYS) {
@ -400,64 +351,90 @@ export class DifyService {
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 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: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
textBuffer = '';
if (mediaType === 'image') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'image',
media: url,
});
} else if (mediaType === 'video') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'video',
media: url,
});
} else if (mediaType === 'audio') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'audio',
media: url,
});
} else if (mediaType === 'document') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'document',
media: url,
fileName: altText || 'file',
});
} 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;
lastIndex = match.index + fullMatch.length;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
const remainingText = message.slice(lastIndex);
if (remainingText) {
textBuffer += remainingText;
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
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 === Integration.WHATSAPP_BAILEYS) {
@ -484,21 +461,35 @@ export class DifyService {
}
}
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: textBuffer.trim(),
},
false,
);
const delay = Math.min(Math.max(textBuffer.length * timePerChar, minDelay), maxDelay);
if (instance.integration === 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: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
sendTelemetry('/message/sendText');
}
private async initNewSession(
protected async initNewSession(
instance: any,
remoteJid: string,
dify: Dify,
@ -506,46 +497,30 @@ export class DifyService {
session: IntegrationSession,
content: string,
pushName?: string,
msg?: any,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: dify.id,
});
if (data.session) {
session = data.session;
}
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
return;
}
public async processDify(
instance: any,
remoteJid: string,
dify: Dify,
session: IntegrationSession,
settings: DifySetting,
content: string,
pushName?: string,
) {
if (session && session.status !== 'opened') {
try {
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
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) {
public async process(
instance: any,
remoteJid: string,
dify: Dify,
session: IntegrationSession,
settings: DifySetting,
content: string,
pushName?: string,
msg?: any,
) {
try {
// Handle keyword finish
if (settings?.keywordFinish?.includes(content.toLowerCase())) {
if (settings?.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
@ -555,73 +530,65 @@ export class DifyService {
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
await this.prismaRepository.integrationSession.delete({
where: {
botId: dify.id,
remoteJid: remoteJid,
id: session.id,
},
});
}
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
await sendTelemetry('/dify/session/finish');
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
return;
}
// If session is new or doesn't exist
if (!session) {
const data = {
remoteJid,
pushName,
botId: dify.id,
};
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,
const createSession = await this.createNewSession(
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
data,
);
sendTelemetry('/message/sendText');
}
return;
}
await this.initNewSession(
instance,
remoteJid,
dify,
settings,
createSession.session,
content,
pushName,
msg,
);
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
await sendTelemetry('/dify/session/start');
return;
}
// If session exists but is paused
if (session.status === 'paused') {
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: {
botId: dify.id,
remoteJid: remoteJid,
status: 'opened',
awaitUser: true,
},
});
return;
}
// Regular message for ongoing session
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
return;
}
}

View File

@ -7,12 +7,11 @@ 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 } from '@prisma/client';
import { getConversationMessage } from '@utils/getConversationMessage';
import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController, ChatbotSettings } from '../../base-chatbot.controller';
export class EvoaiController extends ChatbotController implements ChatbotControllerInterface {
export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto> {
constructor(
private readonly evoaiService: EvoaiService,
prismaRepository: PrismaRepository,
@ -26,6 +25,7 @@ export class EvoaiController extends ChatbotController implements ChatbotControl
}
public readonly logger = new Logger('EvoaiController');
protected readonly integrationName = 'Evoai';
integrationEnabled = configService.get<Evoai>('EVOAI').ENABLED;
botRepository: any;
@ -33,6 +33,51 @@ export class EvoaiController extends ChatbotController implements ChatbotControl
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
protected getFallbackBotId(settings: any): string | undefined {
return settings?.fallbackId;
}
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');
}
}
// Bots
public async createBot(instance: InstanceDto, data: EvoaiDto) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
@ -45,74 +90,6 @@ export class EvoaiController extends ChatbotController implements ChatbotControl
})
.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 a evoai with an "All" trigger, you cannot have more bots while it is active');
}
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -125,71 +102,7 @@ export class EvoaiController extends ChatbotController implements ChatbotControl
throw new Error('Evoai 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,
agentUrl: data.agentUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error creating evoai');
}
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
@ -244,643 +157,17 @@ export class EvoaiController extends ChatbotController implements ChatbotControl
return bot;
}
public async updateBot(instance: InstanceDto, botId: string, 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);
const bot = await this.botRepository.findFirst({
where: {
id: botId,
},
});
if (!bot) {
throw new Error('Evoai not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Evoai not found');
}
if (data.triggerType === 'all') {
const checkTriggerAll = await this.botRepository.findFirst({
where: {
enabled: true,
triggerType: 'all',
id: {
not: botId,
},
instanceId: instanceId,
},
});
if (checkTriggerAll) {
throw new Error('You already have a evoai 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,
agentUrl: data.agentUrl,
apiKey: data.apiKey,
},
});
if (checkDuplicate) {
throw new Error('Evoai 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,
agentUrl: data.agentUrl,
apiKey: data.apiKey,
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
instanceId: instanceId,
triggerType: data.triggerType,
triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue,
ignoreJids: data.ignoreJids,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return bot;
} catch (error) {
this.logger.error(error);
throw new Error('Error updating evoai');
}
}
public async deleteBot(instance: InstanceDto, botId: string) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai 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('Evoai not found');
}
if (bot.instanceId !== instanceId) {
throw new Error('Evoai 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 evoai bot');
}
}
// Settings
public async settings(instance: InstanceDto, data: any) {
if (!this.integrationEnabled) throw new BadRequestException('Evoai 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,
evoaiIdFallback: data.evoaiIdFallback,
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,
evoaiIdFallback: updateSettings.evoaiIdFallback,
ignoreJids: updateSettings.ignoreJids,
splitMessages: updateSettings.splitMessages,
timePerChar: updateSettings.timePerChar,
};
}
const newSetttings = await this.settingsRepository.create({
data: {
expire: data.expire,
keywordFinish: data.keywordFinish,
delayMessage: data.delayMessage,
unknownMessage: data.unknownMessage,
listeningFromMe: data.listeningFromMe,
stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
evoaiIdFallback: data.evoaiIdFallback,
ignoreJids: data.ignoreJids,
instanceId: instanceId,
splitMessages: data.splitMessages,
timePerChar: data.timePerChar,
},
});
return {
expire: newSetttings.expire,
keywordFinish: newSetttings.keywordFinish,
delayMessage: newSetttings.delayMessage,
unknownMessage: newSetttings.unknownMessage,
listeningFromMe: newSetttings.listeningFromMe,
stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime,
evoaiIdFallback: newSetttings.evoaiIdFallback,
ignoreJids: newSetttings.ignoreJids,
splitMessages: newSetttings.splitMessages,
timePerChar: newSetttings.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('Evoai 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,
evoaiIdFallback: '',
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,
evoaiIdFallback: settings.evoaiIdFallback,
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('Evoai 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('Evoai 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('Evoai not found');
}
return await this.sessionRepository.findMany({
where: {
instanceId: instanceId,
remoteJid,
botId: bot ? botId : { not: null },
type: 'evoai',
},
});
} 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('Evoai 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 EvoaiModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (fallback?.evoaiIdFallback) {
const findFallback = await this.botRepository.findFirst({
where: {
id: fallback.evoaiIdFallback,
},
});
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.evoaiService.processEvoai(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
debouncedContent,
msg?.pushName,
msg,
);
});
} else {
await this.evoaiService.processEvoai(
this.waMonitor.waInstances[instance.instanceName],
remoteJid,
findBot,
session,
{
...settings,
expire,
keywordFinish,
delayMessage,
unknownMessage,
listeningFromMe,
stopBotFromMe,
keepOpen,
debounceTime,
ignoreJids,
splitMessages,
timePerChar,
},
content,
msg?.pushName,
msg,
);
}
return;
} catch (error) {
this.logger.error(error);
return;
}
// Process Evoai-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: EvoaiModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,37 +1,12 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvoaiDto {
enabled?: boolean;
description?: string;
export class EvoaiDto extends BaseChatbotDto {
// Evoai specific fields
agentUrl?: string;
apiKey?: string;
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
triggerType?: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
}
export class EvoaiSettingDto {
expire?: number;
keywordFinish?: string;
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
stopBotFromMe?: boolean;
keepOpen?: boolean;
debounceTime?: number;
evoaiIdFallback?: string;
ignoreJids?: any;
splitMessages?: boolean;
timePerChar?: number;
export class EvoaiSettingDto extends BaseChatbotSettingDto {
// Evoai specific fields
}

View File

@ -1,84 +1,85 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService, Language } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { ConfigService } from '@config/env.config';
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
import FormData from 'form-data';
import { v4 as uuidv4 } from 'uuid';
export class EvoaiService {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly prismaRepository: PrismaRepository,
private readonly configService: ConfigService,
) {}
import { BaseChatbotService } from '../../base-chatbot.service';
private readonly logger = new Logger('EvoaiService');
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
configService: ConfigService,
) {
super(waMonitor, prismaRepository, 'EvoaiService', configService);
}
/**
* Return the bot type for EvoAI
*/
protected getBotType(): string {
return 'evoai';
}
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: 'evoai',
},
});
return super.createNewSession(instance, data, 'evoai');
}
return { session };
/**
* Override the process method to directly handle audio messages
*/
public async process(
instance: any,
remoteJid: string,
bot: Evoai,
session: IntegrationSession,
settings: EvoaiSetting,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
try {
this.logger.debug(`[EvoAI] Processing message with custom process method`);
// Check if this is an audio message that we should try to transcribe
if (msg?.messageType === 'audioMessage' && msg?.message?.audioMessage) {
this.logger.debug(`[EvoAI] Detected audio message, attempting transcription`);
try {
// Download the audio using the whole msg object
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
this.logger.debug(`[EvoAI] Downloaded audio: ${mediaBuffer?.length || 0} bytes`);
// Transcribe with OpenAI's Whisper
const transcribedText = await this.speechToText(mediaBuffer);
this.logger.debug(`[EvoAI] Transcription result: ${transcribedText || 'FAILED'}`);
if (transcribedText) {
// Use the transcribed text instead of the original content
this.logger.debug(`[EvoAI] Using transcribed text: ${transcribedText}`);
// Call the parent process method with the transcribed text
return super.process(instance, remoteJid, bot, session, settings, transcribedText, pushName, msg);
}
} catch (err) {
this.logger.error(`[EvoAI] Audio transcription error: ${err}`);
}
}
// For non-audio messages or if transcription failed, proceed normally
return super.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
} catch (error) {
this.logger.error(error);
this.logger.error(`[EvoAI] Error in process: ${error}`);
return;
}
}
private isImageMessage(content: string) {
return content.includes('imageMessage');
}
private isAudioMessage(content: string) {
return content.includes('audioMessage');
}
private async speechToText(audioBuffer: Buffer): Promise<string | null> {
try {
const apiKey = this.configService.get<any>('OPENAI')?.API_KEY;
if (!apiKey) {
this.logger.error('[EvoAI] No OpenAI API key set for Whisper transcription');
return null;
}
const lang = this.configService.get<Language>('LANGUAGE').includes('pt')
? 'pt'
: this.configService.get<Language>('LANGUAGE');
const formData = new FormData();
formData.append('file', audioBuffer, 'audio.ogg');
formData.append('model', 'whisper-1');
formData.append('language', lang);
const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, {
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${apiKey}`,
},
});
return response?.data?.text || null;
} catch (err) {
this.logger.error(`[EvoAI] Whisper transcription failed: ${err}`);
return null;
}
}
private async sendMessageToBot(
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: EvoaiSetting,
@ -89,64 +90,41 @@ export class EvoaiService {
msg?: any,
) {
try {
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
const endpoint: string = evoai.agentUrl;
const callId = `call-${uuidv4()}`;
const taskId = `task-${uuidv4()}`;
// Prepare message parts
const parts: any[] = [
const parts = [
{
type: 'text',
text: content,
},
];
// If content indicates an image/file, fetch and encode as base64, then send as a file part
if ((this.isImageMessage(content) || this.isAudioMessage(content)) && msg) {
const isImage = this.isImageMessage(content);
const isAudio = this.isAudioMessage(content);
this.logger.debug(`[EvoAI] Media message detected: ${content}`);
let transcribedText = null;
if (isAudio) {
try {
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
transcribedText = await this.speechToText(mediaBuffer);
if (transcribedText) {
parts[0].text = transcribedText;
} else {
parts[0].text = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
parts[0].text = '[Audio message could not be transcribed]';
}
} else if (isImage) {
const contentSplit = content.split('|');
parts[0].text = contentSplit[2] || content;
let fileContent = null,
fileName = null,
mimeType = null;
try {
this.logger.debug(
`[EvoAI] Fetching image using downloadMediaMessage with msg.key: ${JSON.stringify(msg.key)}`,
);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
fileContent = Buffer.from(mediaBuffer).toString('base64');
fileName = contentSplit[2] || `${msg.key.id}.jpg`;
mimeType = 'image/jpeg';
parts.push({
type: 'file',
file: {
name: fileName,
bytes: fileContent,
mimeType: mimeType,
},
});
} catch (fileErr) {
this.logger.error(`[EvoAI] Failed to fetch or encode image for EvoAI: ${fileErr}`);
}
// Handle image message if present
if (this.isImageMessage(content) && msg) {
const contentSplit = content.split('|');
parts[0].text = contentSplit[2] || content;
try {
// Download the image
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
const fileContent = Buffer.from(mediaBuffer).toString('base64');
const fileName = contentSplit[2] || `${msg.key?.id || 'image'}.jpg`;
parts.push({
type: 'file',
file: {
name: fileName,
bytes: fileContent,
mimeType: 'image/jpeg',
},
} as any);
} catch (fileErr) {
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
}
}
@ -224,301 +202,4 @@ export class EvoaiService {
return;
}
}
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: EvoaiSetting) {
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 === 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 === 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 === 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 === 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,
evoai: Evoai,
settings: EvoaiSetting,
session: IntegrationSession,
content: string,
pushName?: string,
msg?: any,
) {
const data = await this.createNewSession(instance, {
remoteJid,
pushName,
botId: evoai.id,
});
if (data.session) {
session = data.session;
}
await this.sendMessageToBot(instance, session, settings, evoai, remoteJid, pushName, content, msg);
return;
}
public async processEvoai(
instance: any,
remoteJid: string,
evoai: Evoai,
session: IntegrationSession,
settings: EvoaiSetting,
content: string,
pushName?: string,
msg?: any,
) {
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: evoai.id,
remoteJid: remoteJid,
},
});
}
await this.initNewSession(instance, remoteJid, evoai, settings, session, content, pushName, msg);
return;
}
}
if (!session) {
await this.initNewSession(instance, remoteJid, evoai, settings, session, content, pushName, msg);
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: evoai.id,
remoteJid: remoteJid,
},
});
}
return;
}
await this.sendMessageToBot(instance, session, settings, evoai, remoteJid, pushName, content, msg);
return;
}
}

View File

@ -1,4 +1,3 @@
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';
@ -7,12 +6,11 @@ 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 { IntegrationSession, N8n as N8nModel } from '@prisma/client';
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
import { BaseChatbotController } from '../../base-chatbot.controller';
export class N8nController extends ChatbotController implements ChatbotControllerInterface {
export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
constructor(
private readonly n8nService: N8nService,
prismaRepository: PrismaRepository,
@ -26,6 +24,7 @@ export class N8nController extends ChatbotController implements ChatbotControlle
}
public readonly logger = new Logger('N8nController');
protected readonly integrationName = 'N8n';
integrationEnabled = configService.get('N8N').ENABLED;
botRepository: any;
@ -33,6 +32,54 @@ export class N8nController extends ChatbotController implements ChatbotControlle
sessionRepository: any;
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
protected getFallbackBotId(settings: any): string | undefined {
return settings?.fallbackId;
}
protected getFallbackFieldName(): string {
return 'n8nIdFallback';
}
protected getIntegrationType(): string {
return 'n8n';
}
protected getAdditionalBotData(data: N8nDto): Record<string, any> {
return {
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
};
}
// Implementation for bot-specific updates
protected getAdditionalUpdateFields(data: N8nDto): Record<string, any> {
return {
webhookUrl: data.webhookUrl,
basicAuthUser: data.basicAuthUser,
basicAuthPass: data.basicAuthPass,
};
}
// Implementation for bot-specific duplicate validation on update
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: N8nDto): Promise<void> {
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');
}
}
// Bots
public async createBot(instance: InstanceDto, data: N8nDto) {
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
@ -45,74 +92,7 @@ export class N8nController extends ChatbotController implements ChatbotControlle
})
.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');
}
// Check for N8n-specific duplicate
const checkDuplicate = await this.botRepository.findFirst({
where: {
instanceId: instanceId,
@ -126,72 +106,8 @@ export class N8nController extends ChatbotController implements ChatbotControlle
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');
}
// Let the base class handle the rest of the bot creation process
return super.createBot(instance, data);
}
public async findBot(instance: InstanceDto) {
@ -246,643 +162,17 @@ export class N8nController extends ChatbotController implements ChatbotControlle
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;
}
// Process N8n-specific bot logic
protected async processBot(
instance: any,
remoteJid: string,
bot: N8nModel,
session: IntegrationSession,
settings: any,
content: string,
pushName?: string,
msg?: any,
) {
this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
}
}

View File

@ -1,18 +1,18 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class N8nDto {
enabled?: boolean;
description?: string;
export class N8nDto extends BaseChatbotDto {
// N8n specific fields
webhookUrl?: string;
basicAuthUser?: string;
basicAuthPass?: string;
// Advanced bot properties (copied from DifyDto style)
triggerType?: TriggerType;
triggerType: TriggerType;
triggerOperator?: TriggerOperator;
triggerValue?: string;
expire?: number;
keywordFinish?: string;
keywordFinish?: string[];
delayMessage?: number;
unknownMessage?: string;
listeningFromMe?: boolean;
@ -24,8 +24,8 @@ export class N8nDto {
timePerChar?: number;
}
export class N8nSettingDto {
// Add settings fields here if needed for compatibility
export class N8nSettingDto extends BaseChatbotSettingDto {
// N8n specific fields
}
export class N8nMessageDto {

View File

@ -1,22 +1,29 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { ConfigService } from '@config/env.config';
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
import { sendTelemetry } from '@utils/sendTelemetry';
import axios from 'axios';
import { downloadMediaMessage } from 'baileys';
import { BaseChatbotService } from '../../base-chatbot.service';
import { N8nDto } from '../dto/n8n.dto';
export class N8nService {
private readonly logger = new Logger('N8nService');
private readonly waMonitor: WAMonitoringService;
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
constructor(
waMonitor: WAMonitoringService,
private readonly prismaRepository: PrismaRepository,
prismaRepository: PrismaRepository,
configService: ConfigService,
) {
this.waMonitor = waMonitor;
super(waMonitor, prismaRepository, 'N8nService', configService);
}
/**
* Return the bot type for N8n
*/
protected getBotType(): string {
return 'n8n';
}
/**
@ -122,40 +129,10 @@ export class N8nService {
}
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;
}
return super.createNewSession(instance, data, 'n8n');
}
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(
protected async sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: N8nSetting,
@ -163,6 +140,7 @@ export class N8nService {
remoteJid: string,
pushName: string,
content: string,
msg?: any,
) {
try {
const endpoint: string = n8n.webhookUrl;
@ -170,6 +148,24 @@ export class N8nService {
chatInput: content,
sessionId: session.sessionId,
};
// Handle audio messages
if (this.isAudioMessage(content) && msg) {
try {
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
const mediaBuffer = await downloadMediaMessage({ key: msg.key, message: msg.message }, 'buffer', {});
const transcribedText = await this.speechToText(mediaBuffer);
if (transcribedText) {
payload.chatInput = transcribedText;
} else {
payload.chatInput = '[Audio message could not be transcribed]';
}
} catch (err) {
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
payload.chatInput = '[Audio message could not be transcribed]';
}
}
const headers: Record<string, string> = {};
if (n8n.basicAuthUser && n8n.basicAuthPass) {
const auth = Buffer.from(`${n8n.basicAuthUser}:${n8n.basicAuthPass}`).toString('base64');
@ -193,45 +189,39 @@ export class N8nService {
}
}
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: N8nSetting) {
protected 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 [altText, url] = match;
const mediaType = getMediaType(url);
const [fullMatch, exclamation, altText, url] = match;
const mediaType = this.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(
@ -245,67 +235,103 @@ export class N8nService {
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,
);
const delay = Math.min(Math.max(textBuffer.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: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
textBuffer = '';
}
if (mediaType === 'audio') {
await instance.audioWhatsapp({
textBuffer = '';
if (mediaType === 'image') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
audio: url,
caption: altText,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'image',
media: url,
});
} else if (mediaType === 'video') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'video',
media: url,
});
} else if (mediaType === 'audio') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
mediatype: 'audio',
media: url,
});
} else if (mediaType === 'document') {
await instance.mediaMessage({
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
caption: exclamation === '!' ? undefined : altText,
mediatype: 'document',
media: url,
fileName: altText || 'file',
});
} 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;
lastIndex = match.index + fullMatch.length;
}
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
const remainingText = message.slice(lastIndex);
if (remainingText) {
textBuffer += remainingText;
}
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (textBuffer.trim()) {
const splitMessages = settings.splitMessages ?? false;
const timePerChar = settings.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
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(
@ -319,25 +345,41 @@ export class N8nService {
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,
);
const delay = Math.min(Math.max(textBuffer.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: textBuffer,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === 'WHATSAPP_BAILEYS') {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
sendTelemetry('/message/sendText');
}
private async initNewSession(
protected async initNewSession(
instance: any,
remoteJid: string,
n8n: N8n,
@ -345,88 +387,97 @@ export class N8nService {
session: IntegrationSession,
content: string,
pushName?: string,
msg?: any,
) {
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') {
try {
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
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) {
}
public async process(
instance: any,
remoteJid: string,
n8n: N8n,
session: IntegrationSession,
settings: N8nSetting,
content: string,
pushName?: string,
msg?: any,
) {
try {
// Handle keyword finish
if (settings?.keywordFinish?.includes(content.toLowerCase())) {
if (settings?.keepOpen) {
await this.prismaRepository.integrationSession.update({
where: { id: session.id },
data: { status: 'closed' },
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: { botId: n8n.id, remoteJid: remoteJid },
await this.prismaRepository.integrationSession.delete({
where: {
id: session.id,
},
});
}
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,
// If session is new or doesn't exist
if (!session) {
const data = {
remoteJid,
pushName,
botId: n8n.id,
};
const createSession = await this.createNewSession(
{ instanceName: instance.instanceName, instanceId: instance.instanceId },
data,
);
sendTelemetry('/message/sendText');
await this.initNewSession(
instance,
remoteJid,
n8n,
settings,
createSession.session,
content,
pushName,
msg,
);
await sendTelemetry('/n8n/session/start');
return;
}
return;
}
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
if (settings.keepOpen) {
// If session exists but is paused
if (session.status === 'paused') {
await this.prismaRepository.integrationSession.update({
where: { id: session.id },
data: { status: 'closed' },
});
} else {
await this.prismaRepository.integrationSession.deleteMany({
where: { botId: n8n.id, remoteJid: remoteJid },
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
},
});
return;
}
// Regular message for ongoing session
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName || '', content, msg);
} catch (error) {
this.logger.error(error);
return;
}
await this.sendMessageToBot(instance, session, settings, n8n, remoteJid, pushName, content);
return;
}
}

View File

@ -131,7 +131,7 @@ export const evolutionBotController = new EvolutionBotController(evolutionBotSer
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository);
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
const n8nService = new N8nService(waMonitor, prismaRepository);
const n8nService = new N8nService(waMonitor, prismaRepository, configService);
export const n8nController = new N8nController(n8nService, prismaRepository, waMonitor);
const evoaiService = new EvoaiService(waMonitor, prismaRepository, configService);