diff --git a/src/api/integrations/chatbot/base-chatbot.controller.ts b/src/api/integrations/chatbot/base-chatbot.controller.ts index abe0bc8d..4d061923 100644 --- a/src/api/integrations/chatbot/base-chatbot.controller.ts +++ b/src/api/integrations/chatbot/base-chatbot.controller.ts @@ -31,7 +31,7 @@ export interface BaseBotData { enabled?: boolean; description: string; expire?: number; - keywordFinish?: string[]; + keywordFinish?: string; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; @@ -792,7 +792,7 @@ export abstract class BaseChatbotController { // Forward the message to the chatbot API await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg); + + // Update session to indicate we're waiting for user response + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: true, + }, + }); } catch (error) { this.logger.error(`Error in process: ${error}`); return; @@ -218,12 +229,9 @@ export abstract class BaseChatbotService { 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 [, , altText, url] = match; const mediaType = this.getMediaType(url); const beforeText = message.slice(lastIndex, match.index); @@ -240,38 +248,26 @@ export abstract class BaseChatbotService { // Handle sending the media try { - switch (mediaType) { - case 'image': - await instance.mediaMessage({ + if (mediaType === 'audio') { + await instance.audioWhatsapp({ + number: remoteJid.split('@')[0], + delay: (settings as any)?.delayMessage || 1000, + audio: url, + caption: altText, + }); + } else { + await instance.mediaMessage( + { number: remoteJid.split('@')[0], delay: (settings as any)?.delayMessage || 1000, + mediatype: mediaType, + media: url, 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; + fileName: mediaType === 'document' ? altText || 'document' : undefined, + }, + null, + false, + ); } } catch (error) { this.logger.error(`Error sending media: ${error}`); @@ -283,12 +279,15 @@ export abstract class BaseChatbotService { textBuffer += `[${altText}](${url})`; } - lastIndex = match.index + fullMatch.length; + lastIndex = linkRegex.lastIndex; } // Add any remaining text after the last match if (lastIndex < message.length) { - textBuffer += message.slice(lastIndex); + const remainingText = message.slice(lastIndex); + if (remainingText.trim()) { + textBuffer += remainingText; + } } // Send any remaining text diff --git a/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts b/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts index cd632e80..33c95f11 100644 --- a/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts +++ b/src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts @@ -1,4 +1,3 @@ -import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto'; import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service'; @@ -9,7 +8,7 @@ import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client'; -import { BaseChatbotController, ChatbotSettings } from '../../base-chatbot.controller'; +import { BaseChatbotController } from '../../base-chatbot.controller'; export class EvoaiController extends BaseChatbotController { constructor( @@ -34,7 +33,7 @@ export class EvoaiController extends BaseChatbotController userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; protected getFallbackBotId(settings: any): string | undefined { - return settings?.fallbackId; + return settings?.evoaiIdFallback; } protected getFallbackFieldName(): string { @@ -168,6 +167,6 @@ export class EvoaiController extends BaseChatbotController pushName?: string, msg?: any, ) { - this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg); + await this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg); } } diff --git a/src/api/integrations/chatbot/evolutionBot/controllers/evolutionBot.controller.ts b/src/api/integrations/chatbot/evolutionBot/controllers/evolutionBot.controller.ts index 0d5825de..ff41c4f9 100644 --- a/src/api/integrations/chatbot/evolutionBot/controllers/evolutionBot.controller.ts +++ b/src/api/integrations/chatbot/evolutionBot/controllers/evolutionBot.controller.ts @@ -1,16 +1,13 @@ -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 { EvolutionBot } from '@prisma/client'; -import { getConversationMessage } from '@utils/getConversationMessage'; +import { EvolutionBot, IntegrationSession } from '@prisma/client'; -import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; +import { BaseChatbotController } from '../../base-chatbot.controller'; import { EvolutionBotDto } from '../dto/evolutionBot.dto'; import { EvolutionBotService } from '../services/evolutionBot.service'; -export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface { +export class EvolutionBotController extends BaseChatbotController { constructor( private readonly evolutionBotService: EvolutionBotService, prismaRepository: PrismaRepository, @@ -24,258 +21,49 @@ export class EvolutionBotController extends ChatbotController implements Chatbot } public readonly logger = new Logger('EvolutionBotController'); + protected readonly integrationName = 'EvolutionBot'; - integrationEnabled: boolean; + integrationEnabled = true; // Set to true by default or use config value if available botRepository: any; settingsRepository: any; sessionRepository: any; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - // Bots - public async createBot(instance: InstanceDto, data: EvolutionBotDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); + // Implementation of abstract methods required by BaseChatbotController - 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'); - } - - const checkDuplicate = await this.botRepository.findFirst({ - where: { - instanceId: instanceId, - 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, - 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, - 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 bot'); - } + protected getFallbackBotId(settings: any): string | undefined { + return settings?.botIdFallback; } - public async findBot(instance: InstanceDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const bots = await this.botRepository.findMany({ - where: { - instanceId: instanceId, - }, - }); - - if (!bots.length) { - return null; - } - - return bots; + protected getFallbackFieldName(): string { + return 'botIdFallback'; } - public async fetchBot(instance: InstanceDto, botId: string) { - 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('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot not found'); - } - - return bot; + protected getIntegrationType(): string { + return 'evolution'; } - public async updateBot(instance: InstanceDto, botId: string, data: EvolutionBotDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); + protected getAdditionalBotData(data: EvolutionBotDto): Record { + return { + apiUrl: data.apiUrl, + apiKey: data.apiKey, + }; + } - const bot = await this.botRepository.findFirst({ - where: { - id: botId, - }, - }); - - if (!bot) { - throw new Error('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot 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 bot with an "All" trigger, you cannot have more bots while it is active'); - } - } + // Implementation for bot-specific updates + protected getAdditionalUpdateFields(data: EvolutionBotDto): Record { + return { + apiUrl: data.apiUrl, + apiKey: data.apiKey, + }; + } + // Implementation for bot-specific duplicate validation on update + protected async validateNoDuplicatesOnUpdate( + botId: string, + instanceId: string, + data: EvolutionBotDto, + ): Promise { const checkDuplicate = await this.botRepository.findFirst({ where: { id: { @@ -288,573 +76,20 @@ export class EvolutionBotController extends ChatbotController implements Chatbot }); if (checkDuplicate) { - throw new Error('Bot 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, - 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 bot'); + throw new Error('Evolution Bot already exists'); } } - public async deleteBot(instance: InstanceDto, botId: string) { - 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('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot 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 bot'); - } - } - - // Settings - public async settings(instance: InstanceDto, data: any) { - 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, - botIdFallback: data.botIdFallback, - 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, - botIdFallback: updateSettings.botIdFallback, - 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, - botIdFallback: data.botIdFallback, - ignoreJids: data.ignoreJids, - splitMessages: data.splitMessages, - timePerChar: data.timePerChar, - instanceId: instanceId, - }, - }); - - 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, - botIdFallback: newSetttings.botIdFallback, - 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) { - 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, - botIdFallback: '', - 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, - botIdFallback: settings.botIdFallback, - fallback: settings.Fallback, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching default settings'); - } - } - - // Sessions - public async changeStatus(instance: InstanceDto, data: any) { - 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) { - 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: 'evolution', - }, - }); - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching sessions'); - } - } - - public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { - 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) { - 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 EvolutionBot; - - if (!findBot) { - const fallback = await this.settingsRepository.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (fallback?.botIdFallback) { - const findFallback = await this.botRepository.findFirst({ - where: { - id: fallback.botIdFallback, - }, - }); - - 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.evolutionBotService.processBot( - 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.evolutionBotService.processBot( - 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 bot-specific logic + protected async processBot( + instance: any, + remoteJid: string, + bot: EvolutionBot, + session: IntegrationSession, + settings: any, + content: string, + pushName?: string, + ) { + await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName); } } diff --git a/src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts b/src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts index de2d952c..4ceb0c9a 100644 --- a/src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts +++ b/src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts @@ -1,19 +1,20 @@ import { TriggerOperator, TriggerType } from '@prisma/client'; -export class EvolutionBotDto { +import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; + +export class EvolutionBotDto extends BaseChatbotDto { + apiUrl: string; + apiKey: string; enabled?: boolean; - description?: string; - apiUrl?: string; - apiKey?: string; expire?: number; - keywordFinish?: string; + keywordFinish?: string | null; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; stopBotFromMe?: boolean; keepOpen?: boolean; debounceTime?: number; - triggerType?: TriggerType; + triggerType: TriggerType; triggerOperator?: TriggerOperator; triggerValue?: string; ignoreJids?: any; @@ -21,9 +22,9 @@ export class EvolutionBotDto { timePerChar?: number; } -export class EvolutionBotSettingDto { +export class EvolutionBotSettingDto extends BaseChatbotSettingDto { expire?: number; - keywordFinish?: string; + keywordFinish?: string | null; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; diff --git a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts index 5275f9e1..58c53441 100644 --- a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts +++ b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts @@ -1,428 +1,103 @@ /* 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 { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; -export class EvolutionBotService { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly prismaRepository: PrismaRepository, - ) {} +import { BaseChatbotService } from '../../base-chatbot.service'; - private readonly logger = new Logger('EvolutionBotService'); - - 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: 'evolution', - }, - }); - - return { session }; - } catch (error) { - this.logger.error(error); - return; - } +export class EvolutionBotService extends BaseChatbotService { + constructor(waMonitor: WAMonitoringService, configService: ConfigService, prismaRepository: PrismaRepository) { + super(waMonitor, prismaRepository, 'EvolutionBotService', configService); } - private isImageMessage(content: string) { - return content.includes('imageMessage'); + /** + * Get the bot type identifier + */ + protected getBotType(): string { + return 'evolution'; } - private async sendMessageToBot( + /** + * Send a message to the Evolution Bot API + */ + protected async sendMessageToBot( instance: any, session: IntegrationSession, + settings: EvolutionBotSetting, bot: EvolutionBot, remoteJid: string, pushName: string, content: string, - ) { - const payload: any = { - inputs: { - sessionId: session.id, - remoteJid: remoteJid, - pushName: pushName, - instanceName: instance.instanceName, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, - }, - query: content, - conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, - user: remoteJid, - }; - - if (this.isImageMessage(content)) { - const contentSplit = content.split('|'); - - payload.files = [ - { - type: 'image', - url: contentSplit[1].split('?')[0], + msg?: any, + ): Promise { + try { + const payload: any = { + inputs: { + sessionId: session.id, + remoteJid: remoteJid, + pushName: pushName, + fromMe: msg?.key?.fromMe, + instanceName: instance.instanceName, + serverUrl: this.configService.get('SERVER').URL, + apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, }, - ]; - payload.query = contentSplit[2] || content; - } - - if (instance.integration === Integration.WHATSAPP_BAILEYS) { - await instance.client.presenceSubscribe(remoteJid); - await instance.client.sendPresenceUpdate('composing', remoteJid); - } - - let headers: any = { - 'Content-Type': 'application/json', - }; - - if (bot.apiKey) { - headers = { - ...headers, - Authorization: `Bearer ${bot.apiKey}`, + query: content, + conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId, + user: remoteJid, }; - } - const response = await axios.post(bot.apiUrl, payload, { - headers, - }); + if (this.isImageMessage(content)) { + const contentSplit = content.split('|'); - if (instance.integration === Integration.WHATSAPP_BAILEYS) - await instance.client.sendPresenceUpdate('paused', remoteJid); - - const message = response?.data?.message; - - return message; - } - - private async sendMessageWhatsApp( - instance: any, - remoteJid: string, - session: IntegrationSession, - settings: EvolutionBotSetting, - message: string, - ) { - 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((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((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( + payload.files = [ { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - text: textBuffer.trim(), + type: 'image', + url: contentSplit[1].split('?')[0], }, - false, - ); + ]; + payload.query = contentSplit[2] || content; } - textBuffer = ''; - } - sendTelemetry('/message/sendText'); + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.presenceSubscribe(remoteJid); + await instance.client.sendPresenceUpdate('composing', remoteJid); + } - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'opened', - awaitUser: true, - }, - }); - } + let headers: any = { + 'Content-Type': 'application/json', + }; - private async initNewSession( - instance: any, - remoteJid: string, - bot: EvolutionBot, - settings: EvolutionBotSetting, - session: IntegrationSession, - content: string, - pushName?: string, - ) { - const data = await this.createNewSession(instance, { - remoteJid, - pushName, - botId: bot.id, - }); + if (bot.apiKey) { + headers = { + ...headers, + Authorization: `Bearer ${bot.apiKey}`, + }; + } - if (data.session) { - session = data.session; - } + const response = await axios.post(bot.apiUrl, payload, { + headers, + }); - const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content); + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.sendPresenceUpdate('paused', remoteJid); + } - if (!message) return; + const message = response?.data?.message; - await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message); + if (message) { + // Use the base class method to send the message to WhatsApp + await this.sendMessageWhatsApp(instance, remoteJid, message, settings); + } - return; - } - - public async processBot( - instance: any, - remoteJid: string, - bot: EvolutionBot, - session: IntegrationSession, - settings: EvolutionBotSetting, - content: string, - pushName?: string, - ) { - if (session && session.status !== 'opened') { + // Send telemetry + sendTelemetry('/message/sendText'); + } catch (error) { + this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(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) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: bot.id, - remoteJid: remoteJid, - }, - }); - } - - await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName); - return; - } - } - - if (!session) { - await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName); - return; - } - - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'opened', - awaitUser: false, - }, - }); - - if (!content) { - if (settings.unknownMessage) { - this.waMonitor.waInstances[instance.instanceName].textMessage( - { - number: remoteJid.split('@')[0], - delay: settings.delayMessage || 1000, - text: settings.unknownMessage, - }, - false, - ); - - sendTelemetry('/message/sendText'); - } - return; - } - - if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { - if (settings.keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: bot.id, - remoteJid: remoteJid, - }, - }); - } - return; - } - - const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content); - - if (!message) return; - - await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message); - - return; } } diff --git a/src/api/integrations/chatbot/flowise/controllers/flowise.controller.ts b/src/api/integrations/chatbot/flowise/controllers/flowise.controller.ts index c2d3300b..6dc76b96 100644 --- a/src/api/integrations/chatbot/flowise/controllers/flowise.controller.ts +++ b/src/api/integrations/chatbot/flowise/controllers/flowise.controller.ts @@ -1,16 +1,13 @@ -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 { Flowise } from '@prisma/client'; -import { getConversationMessage } from '@utils/getConversationMessage'; +import { Flowise, IntegrationSession } from '@prisma/client'; -import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller'; +import { BaseChatbotController } from '../../base-chatbot.controller'; import { FlowiseDto } from '../dto/flowise.dto'; import { FlowiseService } from '../services/flowise.service'; -export class FlowiseController extends ChatbotController implements ChatbotControllerInterface { +export class FlowiseController extends BaseChatbotController { constructor( private readonly flowiseService: FlowiseService, prismaRepository: PrismaRepository, @@ -24,258 +21,45 @@ export class FlowiseController extends ChatbotController implements ChatbotContr } public readonly logger = new Logger('FlowiseController'); + protected readonly integrationName = 'Flowise'; - integrationEnabled: boolean; + integrationEnabled = true; // Set to true by default or use config value if available botRepository: any; settingsRepository: any; sessionRepository: any; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - // Bots - public async createBot(instance: InstanceDto, data: FlowiseDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); + // Implementation of abstract methods required by BaseChatbotController - 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 Flowise with an "All" trigger, you cannot have more bots while it is active'); - } - - const checkDuplicate = await this.botRepository.findFirst({ - where: { - instanceId: instanceId, - apiUrl: data.apiUrl, - apiKey: data.apiKey, - }, - }); - - if (checkDuplicate) { - throw new Error('Flowise 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, - 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 bot'); - } + protected getFallbackBotId(settings: any): string | undefined { + return settings?.flowiseIdFallback; } - public async findBot(instance: InstanceDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const bots = await this.botRepository.findMany({ - where: { - instanceId: instanceId, - }, - }); - - if (!bots.length) { - return null; - } - - return bots; + protected getFallbackFieldName(): string { + return 'flowiseIdFallback'; } - public async fetchBot(instance: InstanceDto, botId: string) { - 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('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot not found'); - } - - return bot; + protected getIntegrationType(): string { + return 'flowise'; } - public async updateBot(instance: InstanceDto, botId: string, data: FlowiseDto) { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); + protected getAdditionalBotData(data: FlowiseDto): Record { + return { + apiUrl: data.apiUrl, + apiKey: data.apiKey, + }; + } - const bot = await this.botRepository.findFirst({ - where: { - id: botId, - }, - }); - - if (!bot) { - throw new Error('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot 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 bot with an "All" trigger, you cannot have more bots while it is active'); - } - } + // Implementation for bot-specific updates + protected getAdditionalUpdateFields(data: FlowiseDto): Record { + return { + apiUrl: data.apiUrl, + apiKey: data.apiKey, + }; + } + // Implementation for bot-specific duplicate validation on update + protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise { const checkDuplicate = await this.botRepository.findFirst({ where: { id: { @@ -288,573 +72,20 @@ export class FlowiseController extends ChatbotController implements ChatbotContr }); if (checkDuplicate) { - throw new Error('Bot 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, - 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 bot'); + throw new Error('Flowise already exists'); } } - public async deleteBot(instance: InstanceDto, botId: string) { - 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('Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Bot 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 bot'); - } - } - - // Settings - public async settings(instance: InstanceDto, data: any) { - 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, - flowiseIdFallback: data.flowiseIdFallback, - 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, - flowiseIdFallback: updateSettings.flowiseIdFallback, - 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, - flowiseIdFallback: data.flowiseIdFallback, - 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, - flowiseIdFallback: newSetttings.flowiseIdFallback, - 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) { - 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, - flowiseIdFallback: '', - 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, - flowiseIdFallback: settings.flowiseIdFallback, - fallback: settings.Fallback, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching default settings'); - } - } - - // Sessions - public async changeStatus(instance: InstanceDto, data: any) { - 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) { - 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: 'flowise', - }, - }); - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching sessions'); - } - } - - public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { - 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) { - 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 Flowise; - - if (!findBot) { - const fallback = await this.settingsRepository.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (fallback?.flowiseIdFallback) { - const findFallback = await this.botRepository.findFirst({ - where: { - id: fallback.flowiseIdFallback, - }, - }); - - 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.flowiseService.processBot( - 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.flowiseService.processBot( - 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 bot-specific logic + protected async processBot( + instance: any, + remoteJid: string, + bot: Flowise, + session: IntegrationSession, + settings: any, + content: string, + pushName?: string, + ) { + await this.flowiseService.process(instance, remoteJid, bot, session, settings, content, pushName); } } diff --git a/src/api/integrations/chatbot/flowise/dto/flowise.dto.ts b/src/api/integrations/chatbot/flowise/dto/flowise.dto.ts index 98e612ad..aa52e07d 100644 --- a/src/api/integrations/chatbot/flowise/dto/flowise.dto.ts +++ b/src/api/integrations/chatbot/flowise/dto/flowise.dto.ts @@ -1,19 +1,21 @@ import { TriggerOperator, TriggerType } from '@prisma/client'; -export class FlowiseDto { +import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; + +export class FlowiseDto extends BaseChatbotDto { + apiUrl: string; + apiKey: string; + description: string; + keywordFinish?: string | null; + triggerType: TriggerType; enabled?: boolean; - description?: string; - 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; @@ -21,9 +23,9 @@ export class FlowiseDto { timePerChar?: number; } -export class FlowiseSettingDto { +export class FlowiseSettingDto extends BaseChatbotSettingDto { expire?: number; - keywordFinish?: string; + keywordFinish?: string | null; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; diff --git a/src/api/integrations/chatbot/flowise/services/flowise.service.ts b/src/api/integrations/chatbot/flowise/services/flowise.service.ts index 6e4cdbd5..2eb761cf 100644 --- a/src/api/integrations/chatbot/flowise/services/flowise.service.ts +++ b/src/api/integrations/chatbot/flowise/services/flowise.service.ts @@ -1,425 +1,111 @@ /* 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 { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; -export class FlowiseService { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly prismaRepository: PrismaRepository, - ) {} +import { BaseChatbotService } from '../../base-chatbot.service'; - private readonly logger = new Logger('FlowiseService'); +export class FlowiseService extends BaseChatbotService { + constructor(waMonitor: WAMonitoringService, configService: ConfigService, prismaRepository: PrismaRepository) { + super(waMonitor, prismaRepository, 'FlowiseService', configService); + } - public async createNewSession(instance: InstanceDto, data: any) { + /** + * Get the bot type identifier + */ + protected getBotType(): string { + return 'flowise'; + } + + /** + * Send a message to the Flowise API + */ + protected async sendMessageToBot( + instance: any, + session: IntegrationSession, + settings: FlowiseSetting, + bot: Flowise, + remoteJid: string, + pushName: string, + content: string, + msg?: any, + ): Promise { 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: 'flowise', - }, - }); - - return { session }; - } catch (error) { - this.logger.error(error); - return; - } - } - - private isImageMessage(content: string) { - return content.includes('imageMessage'); - } - - private async sendMessageToBot(instance: any, bot: Flowise, remoteJid: string, pushName: string, content: string) { - const payload: any = { - question: content, - overrideConfig: { - sessionId: remoteJid, - vars: { - remoteJid: remoteJid, - pushName: pushName, - instanceName: instance.instanceName, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, - }, - }, - }; - - if (this.isImageMessage(content)) { - const contentSplit = content.split('|'); - - payload.uploads = [ - { - data: contentSplit[1].split('?')[0], - type: 'url', - name: 'Flowise.png', - mime: 'image/png', - }, - ]; - payload.question = contentSplit[2] || content; - } - - if (instance.integration === Integration.WHATSAPP_BAILEYS) { - await instance.client.presenceSubscribe(remoteJid); - await instance.client.sendPresenceUpdate('composing', remoteJid); - } - - let headers: any = { - 'Content-Type': 'application/json', - }; - - if (bot.apiKey) { - headers = { - ...headers, - Authorization: `Bearer ${bot.apiKey}`, - }; - } - - const endpoint = bot.apiUrl; - - if (!endpoint) return null; - - const response = await axios.post(endpoint, payload, { - headers, - }); - - if (instance.integration === Integration.WHATSAPP_BAILEYS) - await instance.client.sendPresenceUpdate('paused', remoteJid); - - const message = response?.data?.text; - - return message; - } - - private async sendMessageWhatsApp( - instance: any, - remoteJid: string, - session: IntegrationSession, - settings: FlowiseSetting, - message: string, - ) { - 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((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((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(), + const payload: any = { + question: content, + overrideConfig: { + sessionId: remoteJid, + vars: { + remoteJid: remoteJid, + pushName: pushName, + instanceName: instance.instanceName, + serverUrl: this.configService.get('SERVER').URL, + apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, }, - false, - ); + }, + }; + + if (this.isImageMessage(content)) { + const contentSplit = content.split('|'); + + payload.uploads = [ + { + data: contentSplit[1].split('?')[0], + type: 'url', + name: 'Flowise.png', + mime: 'image/png', + }, + ]; + payload.question = contentSplit[2] || content; } - textBuffer = ''; - } - sendTelemetry('/message/sendText'); + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.presenceSubscribe(remoteJid); + await instance.client.sendPresenceUpdate('composing', remoteJid); + } - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'opened', - awaitUser: true, - }, - }); + let headers: any = { + 'Content-Type': 'application/json', + }; - return; - } + if (bot.apiKey) { + headers = { + ...headers, + Authorization: `Bearer ${bot.apiKey}`, + }; + } - private async initNewSession( - instance: any, - remoteJid: string, - bot: Flowise, - settings: FlowiseSetting, - session: IntegrationSession, - content: string, - pushName?: string, - ) { - const data = await this.createNewSession(instance, { - remoteJid, - pushName, - botId: bot.id, - }); + const endpoint = bot.apiUrl; - if (data.session) { - session = data.session; - } - - const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content); - - await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message); - - return; - } - - public async processBot( - instance: any, - remoteJid: string, - bot: Flowise, - session: IntegrationSession, - settings: FlowiseSetting, - content: string, - pushName?: string, - ) { - if (session && session.status !== 'opened') { - return; - } - - if (session && settings.expire && settings.expire > 0) { - const now = Date.now(); - - const sessionUpdatedAt = new Date(session.updatedAt).getTime(); - - const diff = now - sessionUpdatedAt; - - const diffInMinutes = Math.floor(diff / 1000 / 60); - - if (diffInMinutes > settings.expire) { - if (settings.keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: bot.id, - remoteJid: remoteJid, - }, - }); - } - - await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName); + if (!endpoint) { + this.logger.error('No Flowise endpoint defined'); return; } - } - if (!session) { - await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName); - return; - } + const response = await axios.post(endpoint, payload, { + headers, + }); - 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'); + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + await instance.client.sendPresenceUpdate('paused', remoteJid); } - 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: bot.id, - remoteJid: remoteJid, - }, - }); + const message = response?.data?.text; + + if (message) { + // Use the base class method to send the message to WhatsApp + await this.sendMessageWhatsApp(instance, remoteJid, message, settings); } + + // Send telemetry + sendTelemetry('/message/sendText'); + } catch (error) { + this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`); return; } - - const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content); - - await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message); - - return; } } diff --git a/src/api/integrations/chatbot/n8n/dto/n8n.dto.ts b/src/api/integrations/chatbot/n8n/dto/n8n.dto.ts index 95f3a6a2..c26608fd 100644 --- a/src/api/integrations/chatbot/n8n/dto/n8n.dto.ts +++ b/src/api/integrations/chatbot/n8n/dto/n8n.dto.ts @@ -13,7 +13,7 @@ export class N8nDto extends BaseChatbotDto { triggerOperator?: TriggerOperator; triggerValue?: string; expire?: number; - keywordFinish?: string[]; + keywordFinish?: string; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; diff --git a/src/api/integrations/chatbot/n8n/services/n8n.service.ts b/src/api/integrations/chatbot/n8n/services/n8n.service.ts index 39fbf6a8..7046adec 100644 --- a/src/api/integrations/chatbot/n8n/services/n8n.service.ts +++ b/src/api/integrations/chatbot/n8n/services/n8n.service.ts @@ -1,7 +1,7 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { ConfigService } from '@config/env.config'; +import { Auth, ConfigService, HttpServer } from '@config/env.config'; import { IntegrationSession, N8n, N8nSetting } from '@prisma/client'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; @@ -104,26 +104,6 @@ export class N8nService extends BaseChatbotService { } } - /** - * Send a message to the N8n bot webhook. - */ - public async sendMessage(n8nId: string, chatInput: string, sessionId: string): Promise { - try { - const bot = await this.prismaRepository.n8n.findFirst({ where: { id: n8nId, enabled: true } }); - if (!bot) throw new Error('N8n bot not found or not enabled'); - const headers: Record = {}; - if (bot.basicAuthUser && bot.basicAuthPass) { - const auth = Buffer.from(`${bot.basicAuthUser}:${bot.basicAuthPass}`).toString('base64'); - headers['Authorization'] = `Basic ${auth}`; - } - const response = await axios.post(bot.webhookUrl, { chatInput, sessionId }, { headers }); - return response.data.output; - } catch (error) { - this.logger.error(error); - throw new Error('Error sending message to n8n bot'); - } - } - public async createNewSession(instance: InstanceDto, data: any) { return super.createNewSession(instance, data, 'n8n'); } @@ -143,6 +123,12 @@ export class N8nService extends BaseChatbotService { const payload: any = { chatInput: content, sessionId: session.sessionId, + remoteJid: remoteJid, + pushName: pushName, + fromMe: msg?.key?.fromMe, + instanceName: instance.instanceName, + serverUrl: this.configService.get('SERVER').URL, + apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, }; // Handle audio messages diff --git a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts index 44f2005a..17973e97 100644 --- a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts +++ b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts @@ -1,4 +1,3 @@ -import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; import { OpenaiCredsDto, OpenaiDto } from '@api/integrations/chatbot/openai/dto/openai.dto'; import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service'; diff --git a/src/api/integrations/chatbot/openai/services/openai.service.ts b/src/api/integrations/chatbot/openai/services/openai.service.ts index 4dc1fcf8..04be9e74 100644 --- a/src/api/integrations/chatbot/openai/services/openai.service.ts +++ b/src/api/integrations/chatbot/openai/services/openai.service.ts @@ -306,7 +306,24 @@ export class OpenaiService extends BaseChatbotService } // Get thread ID from session or create new thread - const threadId = session.sessionId === remoteJid ? (await this.client.beta.threads.create()).id : session.sessionId; + let threadId = session.sessionId; + + // Create a new thread if one doesn't exist or invalid format + if (!threadId || threadId === remoteJid) { + const newThread = await this.client.beta.threads.create(); + threadId = newThread.id; + + // Save the new thread ID to the session + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + sessionId: threadId, + }, + }); + this.logger.log(`Created new thread ID: ${threadId} for session: ${session.id}`); + } // Add message to thread await this.client.beta.threads.messages.create(threadId, messageData); @@ -334,6 +351,7 @@ export class OpenaiService extends BaseChatbotService } // Extract the response text safely with type checking + let responseText = "I couldn't generate a proper response. Please try again."; try { const messages = response?.data || []; if (messages.length > 0) { @@ -341,7 +359,7 @@ export class OpenaiService extends BaseChatbotService if (messageContent.length > 0) { const textContent = messageContent[0]; if (textContent && 'text' in textContent && textContent.text && 'value' in textContent.text) { - return textContent.text.value; + responseText = textContent.text.value; } } } @@ -349,8 +367,20 @@ export class OpenaiService extends BaseChatbotService this.logger.error(`Error extracting response text: ${error}`); } + // Update session with the thread ID to ensure continuity + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: true, + sessionId: threadId, // Ensure thread ID is saved consistently + }, + }); + // Return fallback message if unable to extract text - return "I couldn't generate a proper response. Please try again."; + return responseText; } /** diff --git a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts index c92fdda3..2d710976 100644 --- a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts +++ b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts @@ -1,4 +1,3 @@ -import { IgnoreJidDto } from '@api/dto/chatbot.dto'; import { InstanceDto } from '@api/dto/instance.dto'; import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto'; import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service'; @@ -8,13 +7,12 @@ import { Events } from '@api/types/wa.types'; import { configService, Typebot } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; -import { Typebot as TypebotModel } from '@prisma/client'; -import { getConversationMessage } from '@utils/getConversationMessage'; +import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client'; import axios from 'axios'; -import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller'; +import { BaseChatbotController } from '../../base-chatbot.controller'; -export class TypebotController extends ChatbotController implements ChatbotControllerInterface { +export class TypebotController extends BaseChatbotController { constructor( private readonly typebotService: TypebotService, prismaRepository: PrismaRepository, @@ -28,6 +26,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr } public readonly logger = new Logger('TypebotController'); + protected readonly integrationName = 'Typebot'; integrationEnabled = configService.get('TYPEBOT').ENABLED; botRepository: any; @@ -35,245 +34,35 @@ export class TypebotController extends ChatbotController implements ChatbotContr sessionRepository: any; userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; - // Bots - public async createBot(instance: InstanceDto, data: TypebotDto) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if ( - !data.expire || - !data.keywordFinish || - !data.delayMessage || - !data.unknownMessage || - !data.listeningFromMe || - !data.stopBotFromMe || - !data.keepOpen || - !data.debounceTime || - !data.ignoreJids - ) { - const defaultSettingCheck = await this.settingsRepository.findFirst({ - where: { - instanceId: instanceId, - }, - }); - - if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; - if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; - if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; - if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; - if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; - if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; - if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; - if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; - if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; - - 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, - }); - } - } - - 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 typebot with an "All" trigger, you cannot have more bots while it is active'); - } - - const checkDuplicate = await this.botRepository.findFirst({ - where: { - url: data.url, - typebot: data.typebot, - instanceId: instanceId, - }, - }); - - if (checkDuplicate) { - throw new Error('Typebot 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, - url: data.url, - typebot: data.typebot, - 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, - }, - }); - - return bot; - } catch (error) { - this.logger.error(error); - throw new Error('Error creating typebot'); - } + protected getFallbackBotId(settings: any): string | undefined { + return settings?.typebotIdFallback; } - public async findBot(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const bots = await this.botRepository.findMany({ - where: { - instanceId: instanceId, - }, - }); - - if (!bots.length) { - return null; - } - - return bots; + protected getFallbackFieldName(): string { + return 'typebotIdFallback'; } - public async fetchBot(instance: InstanceDto, botId: string) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot 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('Typebot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - - return bot; + protected getIntegrationType(): string { + return 'typebot'; } - public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); + protected getAdditionalBotData(data: TypebotDto): Record { + return { + url: data.url, + typebot: data.typebot, + }; + } - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.botRepository.findFirst({ - where: { - id: botId, - }, - }); - - if (!typebot) { - throw new Error('Typebot not found'); - } - - if (typebot.instanceId !== instanceId) { - throw new Error('Typebot 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 typebot with an "All" trigger, you cannot have more bots while it is active', - ); - } - } + // Implementation for bot-specific updates + protected getAdditionalUpdateFields(data: TypebotDto): Record { + return { + url: data.url, + typebot: data.typebot, + }; + } + // Implementation for bot-specific duplicate validation on update + protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise { const checkDuplicate = await this.botRepository.findFirst({ where: { url: data.url, @@ -288,263 +77,39 @@ export class TypebotController extends ChatbotController implements ChatbotContr if (checkDuplicate) { throw new Error('Typebot 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, - url: data.url, - typebot: data.typebot, - expire: data.expire, - keywordFinish: data.keywordFinish, - delayMessage: data.delayMessage, - unknownMessage: data.unknownMessage, - listeningFromMe: data.listeningFromMe, - stopBotFromMe: data.stopBotFromMe, - keepOpen: data.keepOpen, - debounceTime: data.debounceTime, - triggerType: data.triggerType, - triggerOperator: data.triggerOperator, - triggerValue: data.triggerValue, - ignoreJids: data.ignoreJids, - }, - }); - - return bot; - } catch (error) { - this.logger.error(error); - throw new Error('Error updating typebot'); - } } - public async deleteBot(instance: InstanceDto, botId: string) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.botRepository.findFirst({ - where: { - id: botId, - }, - }); - - if (!typebot) { - throw new Error('Typebot not found'); - } - - if (typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - try { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: botId, - }, - }); - - await this.botRepository.delete({ - where: { - id: botId, - }, - }); - - return { typebot: { id: botId } }; - } catch (error) { - this.logger.error(error); - throw new Error('Error deleting typebot'); - } + // Process Typebot-specific bot logic + protected async processBot( + instance: any, + remoteJid: string, + bot: TypebotModel, + session: IntegrationSession, + settings: any, + content: string, + pushName?: string, + msg?: any, + ) { + await this.typebotService.processTypebot( + instance, + remoteJid, + msg, + session, + bot, + bot.url, + settings.expire, + bot.typebot, + settings.keywordFinish, + settings.delayMessage, + settings.unknownMessage, + settings.listeningFromMe, + settings.stopBotFromMe, + settings.keepOpen, + content, + ); } - // Settings - public async settings(instance: InstanceDto, data: any) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot 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, - typebotIdFallback: data.typebotIdFallback, - ignoreJids: data.ignoreJids, - }, - }); - - 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, - typebotIdFallback: updateSettings.typebotIdFallback, - ignoreJids: updateSettings.ignoreJids, - }; - } - - 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, - typebotIdFallback: data.typebotIdFallback, - ignoreJids: data.ignoreJids, - instanceId: instanceId, - }, - }); - - 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, - typebotIdFallback: newSetttings.typebotIdFallback, - ignoreJids: newSetttings.ignoreJids, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error setting default settings'); - } - } - - public async fetchSettings(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot 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: [], - typebotIdFallback: null, - 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, - typebotIdFallback: settings.typebotIdFallback, - fallback: settings.Fallback, - }; - } catch (error) { - this.logger.error(error); - throw new Error('Error fetching default settings'); - } - } - - // Sessions + // TypeBot specific method for starting a bot from API public async startBot(instance: InstanceDto, data: any) { if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); @@ -747,321 +312,4 @@ export class TypebotController extends ChatbotController implements ChatbotContr }, }; } - - public async changeStatus(instance: InstanceDto, data: any) { - if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled'); - - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const remoteJid = data.remoteJid; - const status = data.status; - - const defaultSettingCheck = await this.settingsRepository.findFirst({ - where: { - instanceId, - }, - }); - - if (status === 'delete') { - await this.sessionRepository.deleteMany({ - where: { - remoteJid: remoteJid, - instanceId: instanceId, - botId: { not: null }, - }, - }); - - return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; - } - - if (status === 'closed') { - if (defaultSettingCheck?.keepOpen) { - await this.sessionRepository.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - botId: { not: null }, - }, - data: { - status: status, - }, - }); - } else { - await this.sessionRepository.deleteMany({ - where: { - remoteJid: remoteJid, - instanceId: instanceId, - botId: { not: null }, - }, - }); - } - - return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } }; - } - - const session = await this.sessionRepository.updateMany({ - where: { - instanceId: instanceId, - remoteJid: remoteJid, - botId: { not: null }, - }, - data: { - status: status, - }, - }); - - const typebotData = { - remoteJid: remoteJid, - status: status, - session, - }; - - this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData); - - return { typebot: { ...instance, typebot: typebotData } }; - } 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('Typebot is disabled'); - - try { - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const typebot = await this.botRepository.findFirst({ - where: { - id: botId, - }, - }); - - if (typebot && typebot.instanceId !== instanceId) { - throw new Error('Typebot not found'); - } - - return await this.sessionRepository.findMany({ - where: { - instanceId: instanceId, - remoteJid, - botId: botId ?? { not: null }, - type: 'typebot', - }, - }); - } 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('Typebot 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'); - } - } - - public async emit({ - instance, - remoteJid, - msg, - }: { - instance: InstanceDto; - remoteJid: string; - msg: any; - pushName?: string; - }) { - if (!this.integrationEnabled) return; - - try { - const instanceData = await this.prismaRepository.instance.findFirst({ - where: { - name: instance.instanceName, - }, - }); - - if (!instanceData) throw new Error('Instance not found'); - - const session = await this.getSession(remoteJid, instance); - - const content = getConversationMessage(msg); - - let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as TypebotModel; - - if (!findBot) { - const fallback = await this.settingsRepository.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - if (fallback?.typebotIdFallback) { - const findFallback = await this.botRepository.findFirst({ - where: { - id: fallback.typebotIdFallback, - }, - }); - - findBot = findFallback; - } else { - return; - } - } - - const settings = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - - const url = findBot?.url; - const typebot = findBot?.typebot; - 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; - - 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 (this.checkIgnoreJids(ignoreJids, remoteJid)) return; - - const key = msg.key as { - id: string; - remoteJid: string; - fromMe: boolean; - participant: string; - }; - - if (stopBotFromMe && key.fromMe && session) { - await this.sessionRepository.update({ - where: { - id: session.id, - }, - data: { - status: 'paused', - }, - }); - return; - } - - if (!listeningFromMe && key.fromMe) { - return; - } - - if (debounceTime && debounceTime > 0) { - this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => { - await this.typebotService.processTypebot( - instanceData, - remoteJid, - msg, - session, - findBot, - url, - expire, - typebot, - keywordFinish, - delayMessage, - unknownMessage, - listeningFromMe, - stopBotFromMe, - keepOpen, - debouncedContent, - ); - }); - } else { - await this.typebotService.processTypebot( - instanceData, - remoteJid, - msg, - session, - findBot, - url, - expire, - typebot, - keywordFinish, - delayMessage, - unknownMessage, - listeningFromMe, - stopBotFromMe, - keepOpen, - content, - ); - } - - if (session && !session.awaitUser) return; - } catch (error) { - this.logger.error(error); - return; - } - } } diff --git a/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts b/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts index a7565236..ea7590fd 100644 --- a/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts +++ b/src/api/integrations/chatbot/typebot/dto/typebot.dto.ts @@ -1,5 +1,7 @@ import { TriggerOperator, TriggerType } from '@prisma/client'; +import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; + export class PrefilledVariables { remoteJid?: string; pushName?: string; @@ -7,28 +9,27 @@ export class PrefilledVariables { additionalData?: { [key: string]: any }; } -export class TypebotDto { - enabled?: boolean; - description?: string; +export class TypebotDto extends BaseChatbotDto { url: string; - typebot?: string; + typebot: string; + description: string; expire?: number; - keywordFinish?: string; + keywordFinish?: string | null; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; stopBotFromMe?: boolean; keepOpen?: boolean; debounceTime?: number; - triggerType?: TriggerType; + triggerType: TriggerType; triggerOperator?: TriggerOperator; triggerValue?: string; ignoreJids?: any; } -export class TypebotSettingDto { +export class TypebotSettingDto extends BaseChatbotSettingDto { expire?: number; - keywordFinish?: string; + keywordFinish?: string | null; delayMessage?: number; unknownMessage?: string; listeningFromMe?: boolean; diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index 94a24f4f..7be3cae7 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -1,96 +1,215 @@ import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; import { Auth, ConfigService, HttpServer, Typebot } from '@config/env.config'; -import { Logger } from '@config/logger.config'; import { Instance, IntegrationSession, Message, Typebot as TypebotModel } from '@prisma/client'; -import { getConversationMessage } from '@utils/getConversationMessage'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; -export class TypebotService { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly prismaRepository: PrismaRepository, - ) {} +import { BaseChatbotService } from '../../base-chatbot.service'; - private readonly logger = new Logger('TypebotService'); +export class TypebotService extends BaseChatbotService { + constructor(waMonitor: WAMonitoringService, configService: ConfigService, prismaRepository: PrismaRepository) { + super(waMonitor, prismaRepository, 'TypebotService', configService); + } - public async createNewSession(instance: Instance, data: any) { - if (data.remoteJid === 'status@broadcast') return; + /** + * Get the bot type identifier + */ + protected getBotType(): string { + return 'typebot'; + } + + /** + * Send a message to the Typebot API + */ + protected async sendMessageToBot( + instance: any, + session: IntegrationSession, + settings: any, + bot: TypebotModel, + remoteJid: string, + pushName: string, + content: string, + msg?: any, + ): Promise { + try { + // Initialize a new session if needed or content is special command + if (!session || content === 'init') { + const prefilledVariables = content === 'init' ? msg : null; + await this.initTypebotSession(instance, session, bot, remoteJid, pushName, prefilledVariables); + return; + } + + // Handle keyword matching - if it's a keyword to finish + 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: bot.id, remoteJid: remoteJid }, + }); + } + return; + } + + // Continue an existing chat + const version = this.configService?.get('TYPEBOT').API_VERSION; + let url: string; + let reqData: {}; + + if (version === 'latest') { + url = `${bot.url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`; + reqData = { message: content }; + } else { + url = `${bot.url}/api/v1/sendMessage`; + reqData = { + message: content, + sessionId: session.sessionId.split('-')[1], + }; + } + + const response = await axios.post(url, reqData); + + // Process the response and send the messages to WhatsApp + await this.sendWAMessage( + instance, + session, + settings, + remoteJid, + response?.data?.messages, + response?.data?.input, + response?.data?.clientSideActions, + ); + + // Send telemetry data + sendTelemetry('/message/sendText'); + } catch (error) { + this.logger.error(`Error in sendMessageToBot for Typebot: ${error.message || JSON.stringify(error)}`); + } + } + + /** + * Initialize a new Typebot session + */ + private async initTypebotSession( + instance: any, + session: IntegrationSession, + bot: TypebotModel, + remoteJid: string, + pushName: string, + prefilledVariables?: any, + ): Promise { const id = Math.floor(Math.random() * 10000000000).toString(); try { - const version = this.configService.get('TYPEBOT').API_VERSION; + const version = this.configService?.get('TYPEBOT').API_VERSION; let url: string; let reqData: {}; - if (version === 'latest') { - url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; + if (version === 'latest') { + url = `${bot.url}/api/v1/typebots/${bot.typebot}/startChat`; reqData = { prefilledVariables: { - ...data.prefilledVariables, - remoteJid: data.remoteJid, - pushName: data.pushName || data.prefilledVariables?.pushName || '', + ...(prefilledVariables || {}), + remoteJid: remoteJid, + pushName: pushName || '', instanceName: instance.name, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + serverUrl: this.configService?.get('SERVER').URL, + apiKey: this.configService?.get('AUTHENTICATION').API_KEY.KEY, ownerJid: instance.number, }, }; } else { - url = `${data.url}/api/v1/sendMessage`; - + url = `${bot.url}/api/v1/sendMessage`; reqData = { startParams: { - publicId: data.typebot, + publicId: bot.typebot, prefilledVariables: { - ...data.prefilledVariables, - remoteJid: data.remoteJid, - pushName: data.pushName || data.prefilledVariables?.pushName || '', + ...(prefilledVariables || {}), + remoteJid: remoteJid, + pushName: pushName || '', instanceName: instance.name, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + serverUrl: this.configService?.get('SERVER').URL, + apiKey: this.configService?.get('AUTHENTICATION').API_KEY.KEY, ownerJid: instance.number, }, }, }; } + const request = await axios.post(url, reqData); - let session = null; + // Create or update session with the Typebot session ID + let updatedSession = session; if (request?.data?.sessionId) { - session = await this.prismaRepository.integrationSession.create({ - data: { - remoteJid: data.remoteJid, - pushName: data.pushName || '', - sessionId: `${id}-${request.data.sessionId}`, - status: 'opened', - parameters: { - ...data.prefilledVariables, - remoteJid: data.remoteJid, - pushName: data.pushName || '', - instanceName: instance.name, - serverUrl: this.configService.get('SERVER').URL, - apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, - ownerJid: instance.number, + if (session) { + updatedSession = await this.prismaRepository.integrationSession.update({ + where: { id: session.id }, + data: { + sessionId: `${id}-${request.data.sessionId}`, + status: 'opened', + awaitUser: false, }, - awaitUser: false, - botId: data.botId, - instanceId: instance.id, - type: 'typebot', - }, - }); + }); + } else { + updatedSession = await this.prismaRepository.integrationSession.create({ + data: { + remoteJid: remoteJid, + pushName: pushName || '', + sessionId: `${id}-${request.data.sessionId}`, + status: 'opened', + parameters: { + ...(prefilledVariables || {}), + remoteJid: remoteJid, + pushName: pushName || '', + instanceName: instance.name, + serverUrl: this.configService?.get('SERVER').URL, + apiKey: this.configService?.get('AUTHENTICATION').API_KEY.KEY, + ownerJid: instance.number, + }, + awaitUser: false, + botId: bot.id, + instanceId: instance.id, + type: 'typebot', + }, + }); + } + } + + if (request?.data?.messages?.length > 0) { + // Process the response and send the messages to WhatsApp + await this.sendWAMessage( + instance, + updatedSession, + { + expire: bot.expire, + keywordFinish: bot.keywordFinish, + delayMessage: bot.delayMessage, + unknownMessage: bot.unknownMessage, + listeningFromMe: bot.listeningFromMe, + stopBotFromMe: bot.stopBotFromMe, + keepOpen: bot.keepOpen, + }, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); } - return { ...request.data, session }; } catch (error) { - this.logger.error(error); - return; + this.logger.error(`Error initializing Typebot session: ${error.message || JSON.stringify(error)}`); } } + /** + * Send WhatsApp message with Typebot responses + * This handles the specific formatting and structure of Typebot responses + */ public async sendWAMessage( - instance: Instance, + instance: any, session: IntegrationSession, settings: { expire: number; @@ -106,478 +225,294 @@ export class TypebotService { input: any, clientSideActions: any, ) { - processMessages( - this.waMonitor.waInstances[instance.name], - session, - settings, - messages, - input, - clientSideActions, - applyFormatting, - this.prismaRepository, - ).catch((err) => { - console.error('Erro ao processar mensagens:', err); - }); - - function findItemAndGetSecondsToWait(array, targetId) { - if (!array) return null; - - for (const item of array) { - if (item.lastBubbleBlockId === targetId) { - return item.wait?.secondsToWaitFor; - } - } - return null; + if (!messages || messages.length === 0) { + return; } - function applyFormatting(element) { - let text = ''; + try { + await this.processTypebotMessages(instance, session, settings, remoteJid, messages, input, clientSideActions); + } catch (err) { + this.logger.error(`Error processing Typebot messages: ${err}`); + } + } - if (element.text) { - text += element.text; - } + /** + * Process Typebot-specific message formats and send to WhatsApp + */ + private async processTypebotMessages( + instance: any, + session: IntegrationSession, + settings: { + expire: number; + keywordFinish: string; + delayMessage: number; + unknownMessage: string; + listeningFromMe: boolean; + stopBotFromMe: boolean; + keepOpen: boolean; + }, + remoteJid: string, + messages: any, + input: any, + clientSideActions: any, + ) { + // Helper to find an item in an array and calculate wait time based on delay settings + const findItemAndGetSecondsToWait = (array, targetId) => { + const index = array.findIndex((item) => item.id === targetId); + if (index === -1) return 0; + return index * (settings.delayMessage || 0); + }; - if (element.children && element.type !== 'a') { - for (const child of element.children) { - text += applyFormatting(child); - } - } + // Helper to apply formatting to message content + const applyFormatting = (element) => { + if (!element) return ''; - if (element.type === 'p' && element.type !== 'inline-variable') { - text = text.trim() + '\n'; - } + let formattedText = ''; - if (element.type === 'inline-variable') { - text = text.trim(); - } + if (typeof element === 'string') { + formattedText = element; + } else if (element.text) { + formattedText = element.text; + } else if (element.type === 'text' && element.content) { + formattedText = element.content.text || ''; + } else if (element.content && element.content.richText) { + // Handle Typebot's rich text format + formattedText = element.content.richText.reduce((acc, item) => { + let text = item.text || ''; - if (element.type === 'ol') { - text = - '\n' + - text - .split('\n') - .map((line, index) => (line ? `${index + 1}. ${line}` : '')) - .join('\n'); - } + // Apply bold formatting + if (item.bold) text = `*${text}*`; - if (element.type === 'li') { - text = text - .split('\n') - .map((line) => (line ? ` ${line}` : '')) - .join('\n'); - } + // Apply italic formatting + if (item.italic) text = `_${text}_`; - let formats = ''; + // Apply strikethrough formatting (if supported) + if (item.strikethrough) text = `~${text}~`; - if (element.bold) { - formats += '*'; - } + // Apply URL if present (convert to Markdown style link) + if (item.url) text = `[${text}](${item.url})`; - if (element.italic) { - formats += '_'; - } - - if (element.underline) { - formats += '~'; - } - - let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`; - - if (element.url) { - formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`; + return acc + text; + }, ''); } return formattedText; - } + }; - async function processMessages( - instance: any, - session: IntegrationSession, - settings: { - expire: number; - keywordFinish: string; - delayMessage: number; - unknownMessage: string; - listeningFromMe: boolean; - stopBotFromMe: boolean; - keepOpen: boolean; - }, - messages: any, - input: any, - clientSideActions: any, - applyFormatting: any, - prismaRepository: PrismaRepository, - ) { - for (const message of messages) { - if (message.type === 'text') { - let formattedText = ''; + // Process each message + for (const message of messages) { + // Handle text type messages + if (message.type === 'text') { + const wait = findItemAndGetSecondsToWait(messages, message.id); + const content = applyFormatting(message); - for (const richText of message.content.richText) { - for (const element of richText.children) { - formattedText += applyFormatting(element); - } - formattedText += '\n'; - } + // Skip empty messages + if (!content) continue; - formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, ''); + // Check for WhatsApp list format + const listMatch = content.match(/\[list:(.+?)\]\[(.*?)\]/s); + if (listMatch) { + const { sections, buttonText } = this.parseListFormat(content); - formattedText = formattedText.replace(/\n$/, ''); + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - if (formattedText.includes('[list]')) { - const listJson = { - number: remoteJid.split('@')[0], - title: '', - description: '', - buttonText: '', - footerText: '', - sections: [], - }; - - const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/); - const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/); - const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/); - const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/); - - if (titleMatch) listJson.title = titleMatch[1].trim(); - if (descriptionMatch) listJson.description = descriptionMatch[1].trim(); - if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim(); - if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim(); - - const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1]; - if (menuContent) { - const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g); - if (sections) { - sections.forEach((section) => { - const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(); - const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g); - - const sectionData = { - title: sectionTitle, - rows: - rows?.map((row) => ({ - title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(), - description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(), - rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(), - })) || [], - }; - - listJson.sections.push(sectionData); - }); - } - } - - await instance.listMessage(listJson); - } else if (formattedText.includes('[buttons]')) { - const buttonJson = { - number: remoteJid.split('@')[0], - thumbnailUrl: undefined, - title: '', - description: '', - footer: '', - buttons: [], - }; - - const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/); - const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/); - const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/); - const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/); - - if (titleMatch) buttonJson.title = titleMatch[1].trim(); - if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim(); - if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim(); - if (footerMatch) buttonJson.footer = footerMatch[1].trim(); - - const buttonTypes = { - reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - }; - - for (const [type, pattern] of Object.entries(buttonTypes)) { - let match; - while ((match = pattern.exec(formattedText)) !== null) { - const content = match[1].trim(); - const button: any = { type }; - - switch (type) { - case 'pix': - button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'reply': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'copy': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'call': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'url': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - } - - if (Object.keys(button).length > 1) { - buttonJson.buttons.push(button); - } - } - } - - await instance.buttonMessage(buttonJson); - } else { - await instance.textMessage( - { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - text: formattedText, - }, - false, - ); - } - - sendTelemetry('/message/sendText'); + // Send as WhatsApp list + // Using instance directly since waMonitor might not have sendListMessage + await instance.sendListMessage({ + number: remoteJid.split('@')[0], + sections, + buttonText, + }); + continue; } - if (message.type === 'image') { - await instance.mediaMessage( - { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - mediatype: 'image', - media: message.content.url, - }, - null, - false, - ); + // Check for WhatsApp button format + const buttonMatch = content.match(/\[button:(.+?)\]/); + if (buttonMatch) { + const { text, buttons } = this.parseButtonFormat(content); - sendTelemetry('/message/sendMedia'); + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); + + // Send as WhatsApp buttons + await instance.sendButtonMessage({ + number: remoteJid.split('@')[0], + text, + buttons, + }); + continue; } - if (message.type === 'video') { - await instance.mediaMessage( - { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - mediatype: 'video', - media: message.content.url, - }, - null, - false, - ); - - sendTelemetry('/message/sendMedia'); - } - - if (message.type === 'audio') { - await instance.audioWhatsapp( - { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - encoding: true, - audio: message.content.url, - }, - false, - ); - - sendTelemetry('/message/sendWhatsAppAudio'); - } - - const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); - - if (wait) { - await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - } + // Process for standard text messages + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); + await this.sendMessageWhatsApp(instance, remoteJid, content, settings); } - console.log('input', input); - if (input) { - if (input.type === 'choice input') { - let formattedText = ''; + // Handle image type messages + else if (message.type === 'image') { + const url = message.content?.url || message.content?.imageUrl || ''; + if (!url) continue; - const items = input.items; + const caption = message.content?.caption || ''; + const wait = findItemAndGetSecondsToWait(messages, message.id); - for (const item of items) { - formattedText += `▶️ ${item.content}\n`; - } + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - formattedText = formattedText.replace(/\n$/, ''); + // Send image to WhatsApp + await instance.sendMediaMessage({ + number: remoteJid.split('@')[0], + type: 'image', + media: url, + caption, + }); + } - if (formattedText.includes('[list]')) { - const listJson = { - number: remoteJid.split('@')[0], - title: '', - description: '', - buttonText: '', - footerText: '', - sections: [], - }; + // Handle other media types (video, audio, etc.) + else if (['video', 'audio', 'file'].includes(message.type)) { + const mediaType = message.type; + const url = message.content?.url || ''; + if (!url) continue; - const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/); - const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/); - const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/); - const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/); + const caption = message.content?.caption || ''; + const wait = findItemAndGetSecondsToWait(messages, message.id); - if (titleMatch) listJson.title = titleMatch[1].trim(); - if (descriptionMatch) listJson.description = descriptionMatch[1].trim(); - if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim(); - if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim(); + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1]; - if (menuContent) { - const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g); - if (sections) { - sections.forEach((section) => { - const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(); - const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g); + // Send media to WhatsApp + await instance.sendMediaMessage({ + number: remoteJid.split('@')[0], + type: mediaType, + media: url, + caption, + }); + } + } - const sectionData = { - title: sectionTitle, - rows: - rows?.map((row) => ({ - title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(), - description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(), - rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(), - })) || [], - }; - - listJson.sections.push(sectionData); - }); - } - } - - await instance.listMessage(listJson); - } else if (formattedText.includes('[buttons]')) { - const buttonJson = { - number: remoteJid.split('@')[0], - thumbnailUrl: undefined, - title: '', - description: '', - footer: '', - buttons: [], - }; - - const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/); - const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/); - const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/); - const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/); - - if (titleMatch) buttonJson.title = titleMatch[1].trim(); - if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim(); - if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim(); - if (footerMatch) buttonJson.footer = footerMatch[1].trim(); - - const buttonTypes = { - reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g, - }; - - for (const [type, pattern] of Object.entries(buttonTypes)) { - let match; - while ((match = pattern.exec(formattedText)) !== null) { - const content = match[1].trim(); - const button: any = { type }; - - switch (type) { - case 'pix': - button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'reply': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'copy': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'call': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - - case 'url': - button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim(); - button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim(); - break; - } - - if (Object.keys(button).length > 1) { - buttonJson.buttons.push(button); - } - } - } - - await instance.buttonMessage(buttonJson); - } else { - await instance.textMessage( - { - number: remoteJid.split('@')[0], - delay: settings?.delayMessage || 1000, - text: formattedText, - }, - false, - ); - } - - sendTelemetry('/message/sendText'); - } - - await prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - awaitUser: true, - }, + // Check if we need to update the session status based on input/client actions + if (input && input.type === 'choice input') { + await this.prismaRepository.integrationSession.update({ + where: { id: session.id }, + data: { awaitUser: true }, + }); + } else if (!input && !clientSideActions) { + // If no input or actions, close the session or keep it open based on settings + if (settings.keepOpen) { + await this.prismaRepository.integrationSession.update({ + where: { id: session.id }, + data: { status: 'closed' }, }); } else { - if (!settings?.keepOpen) { - await prismaRepository.integrationSession.deleteMany({ - where: { - id: session.id, - }, - }); - } else { - await prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } + await this.prismaRepository.integrationSession.deleteMany({ + where: { id: session.id }, + }); } } } + /** + * Parse WhatsApp list format from Typebot text + */ + private parseListFormat(text: string): { sections: any[]; buttonText: string } { + try { + const regex = /\[list:(.+?)\]\[(.*?)\]/s; + const match = regex.exec(text); + + if (!match) return { sections: [], buttonText: 'Menu' }; + + const listContent = match[1]; + const buttonText = match[2] || 'Menu'; + + // Parse list sections from content + const sectionStrings = listContent.split(/(?=\{section:)/s); + const sections = []; + + for (const sectionString of sectionStrings) { + if (!sectionString.trim()) continue; + + const sectionMatch = sectionString.match(/\{section:(.+?)\}\[(.*?)\]/s); + if (!sectionMatch) continue; + + const title = sectionMatch[1]; + const rowsContent = sectionMatch[2]; + + const rows = rowsContent + .split(/(?=\[row:)/s) + .map((rowString) => { + const rowMatch = rowString.match(/\[row:(.+?)\]\[(.+?)\]/); + if (!rowMatch) return null; + + return { + title: rowMatch[1], + id: rowMatch[2], + description: '', + }; + }) + .filter(Boolean); + + if (rows.length > 0) { + sections.push({ + title, + rows, + }); + } + } + + return { sections, buttonText }; + } catch (error) { + this.logger.error(`Error parsing list format: ${error}`); + return { sections: [], buttonText: 'Menu' }; + } + } + + /** + * Parse WhatsApp button format from Typebot text + */ + private parseButtonFormat(text: string): { text: string; buttons: any[] } { + try { + const regex = /\[button:(.+?)\]/g; + let match; + const buttons = []; + let cleanedText = text; + + // Extract all button definitions and build buttons array + while ((match = regex.exec(text)) !== null) { + const buttonParts = match[1].split('|'); + if (buttonParts.length >= 1) { + const buttonText = buttonParts[0].trim(); + const buttonId = buttonParts.length > 1 ? buttonParts[1].trim() : buttonText; + + buttons.push({ + buttonId, + buttonText: { displayText: buttonText }, + type: 1, + }); + + // Remove button definition from clean text + cleanedText = cleanedText.replace(match[0], ''); + } + } + + cleanedText = cleanedText.trim(); + + return { + text: cleanedText, + buttons, + }; + } catch (error) { + this.logger.error(`Error parsing button format: ${error}`); + return { text, buttons: [] }; + } + } + + /** + * Main process method for handling Typebot messages + * This is called directly from the controller + */ public async processTypebot( instance: Instance, remoteJid: string, msg: Message, session: IntegrationSession, - findTypebot: TypebotModel, + bot: TypebotModel, url: string, expire: number, typebot: string, @@ -590,368 +525,21 @@ export class TypebotService { content: string, prefilledVariables?: any, ) { - if (session && expire && 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 > expire) { - if (keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: findTypebot.id, - remoteJid: remoteJid, - }, - }); - } - - const data = await this.createNewSession(instance, { - enabled: findTypebot?.enabled, - url: url, - typebot: typebot, - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - remoteJid: remoteJid, - pushName: msg.pushName, - botId: findTypebot.id, - prefilledVariables: prefilledVariables, - }); - - if (data.session) { - session = data.session; - } - - if (data.messages.length === 0) { - const content = getConversationMessage(msg.message); - - if (!content) { - if (unknownMessage) { - this.waMonitor.waInstances[instance.name].textMessage( - { - number: remoteJid.split('@')[0], - delay: delayMessage || 1000, - text: unknownMessage, - }, - false, - ); - - sendTelemetry('/message/sendText'); - } - return; - } - - if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { - if (keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: findTypebot.id, - remoteJid: remoteJid, - }, - }); - } - return; - } - - try { - const version = this.configService.get('TYPEBOT').API_VERSION; - let urlTypebot: string; - let reqData: {}; - if (version === 'latest') { - urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`; - reqData = { - message: content, - }; - } else { - urlTypebot = `${url}/api/v1/sendMessage`; - reqData = { - message: content, - sessionId: data.sessionId, - }; - } - - const request = await axios.post(urlTypebot, reqData); - - await this.sendWAMessage( - instance, - session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); - } catch (error) { - this.logger.error(error); - return; - } - } - - await this.sendWAMessage( - instance, - session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - data.messages, - data.input, - data.clientSideActions, - ); - - return; - } - } - - if (session && !session.awaitUser) { - return; - } - - if (session && session.status !== 'opened') { - return; - } - - if (!session) { - const data = await this.createNewSession(instance, { - enabled: findTypebot?.enabled, - url: url, - typebot: typebot, - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - remoteJid: remoteJid, - pushName: msg?.pushName, - botId: findTypebot.id, - prefilledVariables: prefilledVariables, - }); - - if (data?.session) { - session = data.session; - } - - await this.sendWAMessage( - instance, - session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - data?.messages, - data?.input, - data?.clientSideActions, - ); - - if (data.messages.length === 0) { - if (!content) { - if (unknownMessage) { - this.waMonitor.waInstances[instance.name].textMessage( - { - number: remoteJid.split('@')[0], - delay: delayMessage || 1000, - text: unknownMessage, - }, - false, - ); - - sendTelemetry('/message/sendText'); - } - return; - } - - if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { - if (keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: findTypebot.id, - remoteJid: remoteJid, - }, - }); - } - - return; - } - - let request: any; - try { - const version = this.configService.get('TYPEBOT').API_VERSION; - let urlTypebot: string; - let reqData: {}; - if (version === 'latest') { - urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`; - reqData = { - message: content, - }; - } else { - urlTypebot = `${url}/api/v1/sendMessage`; - reqData = { - message: content, - sessionId: data.sessionId, - }; - } - request = await axios.post(urlTypebot, reqData); - - await this.sendWAMessage( - instance, - session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); - } catch (error) { - this.logger.error(error); - return; - } - } - return; - } - - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'opened', - awaitUser: false, - }, - }); - - if (!content) { - if (unknownMessage) { - this.waMonitor.waInstances[instance.name].textMessage( - { - number: remoteJid.split('@')[0], - delay: delayMessage || 1000, - text: unknownMessage, - }, - false, - ); - - sendTelemetry('/message/sendText'); - } - return; - } - - if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { - if (keepOpen) { - await this.prismaRepository.integrationSession.update({ - where: { - id: session.id, - }, - data: { - status: 'closed', - }, - }); - } else { - await this.prismaRepository.integrationSession.deleteMany({ - where: { - botId: findTypebot.id, - remoteJid: remoteJid, - }, - }); - } - return; - } - - const version = this.configService.get('TYPEBOT').API_VERSION; - let urlTypebot: string; - let reqData: {}; - if (version === 'latest') { - urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`; - reqData = { - message: content, - }; - } else { - urlTypebot = `${url}/api/v1/sendMessage`; - reqData = { - message: content, - sessionId: session.sessionId.split('-')[1], + try { + const settings = { + expire, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, }; + + // Use the base class process method to handle the message + await this.process(instance, remoteJid, bot, session, settings, content, msg.pushName, prefilledVariables || msg); + } catch (error) { + this.logger.error(`Error in processTypebot: ${error}`); } - const request = await axios.post(urlTypebot, reqData); - - await this.sendWAMessage( - instance, - session, - { - expire: expire, - keywordFinish: keywordFinish, - delayMessage: delayMessage, - unknownMessage: unknownMessage, - listeningFromMe: listeningFromMe, - stopBotFromMe: stopBotFromMe, - keepOpen: keepOpen, - }, - remoteJid, - request?.data?.messages, - request?.data?.input, - request?.data?.clientSideActions, - ); - - return; } }