From f9567fbeaa05d19911991d86679be5575806cc79 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 May 2025 17:02:24 -0300 Subject: [PATCH] refactor(chatbot): unify keywordFinish type and enhance session handling - Changed the type of `keywordFinish` from an array to a string in multiple DTOs and controller interfaces to simplify data handling. - Updated the `BaseChatbotService` to include logic for updating session status to 'opened' and managing user responses more effectively. - Refactored the media message handling in the `BaseChatbotService` to streamline the process and improve readability. - Enhanced error logging across various services to ensure better traceability during operations. This commit focuses on improving the structure and consistency of chatbot integrations while ensuring that session management is robust and user-friendly. --- .../chatbot/base-chatbot.controller.ts | 4 +- .../integrations/chatbot/base-chatbot.dto.ts | 4 +- .../chatbot/base-chatbot.service.ts | 69 +- .../evoai/controllers/evoai.controller.ts | 7 +- .../controllers/evolutionBot.controller.ts | 851 +---------- .../evolutionBot/dto/evolutionBot.dto.ts | 17 +- .../services/evolutionBot.service.ts | 453 +----- .../flowise/controllers/flowise.controller.ts | 847 +---------- .../chatbot/flowise/dto/flowise.dto.ts | 18 +- .../flowise/services/flowise.service.ts | 476 ++----- .../integrations/chatbot/n8n/dto/n8n.dto.ts | 2 +- .../chatbot/n8n/services/n8n.service.ts | 28 +- .../openai/controllers/openai.controller.ts | 1 - .../chatbot/openai/services/openai.service.ts | 36 +- .../typebot/controllers/typebot.controller.ts | 860 +---------- .../chatbot/typebot/dto/typebot.dto.ts | 17 +- .../typebot/services/typebot.service.ts | 1268 ++++++----------- 17 files changed, 819 insertions(+), 4139 deletions(-) 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; } }