From bc451e8493a4d6f6f80c90a6e85d592c4995cfae Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 12 Jun 2025 13:24:25 -0300 Subject: [PATCH] feat(Typebot): add splitMessages and timePerChar fields to Typebot models - Introduced `splitMessages` and `timePerChar` fields in the Typebot and TypebotSetting models with default values. - Created a migration script to update the database schema accordingly. - Updated audio message handling to prepend `[audio]` to transcriptions for better clarity in message context. --- .../migration.sql | 7 + prisma/postgresql-schema.prisma | 4 + .../evolution/evolution.channel.service.ts | 2 +- .../channel/meta/whatsapp.business.service.ts | 8 +- .../whatsapp/whatsapp.baileys.service.ts | 4 +- .../chatbot/base-chatbot.controller.ts | 2 +- .../chatbot/evoai/services/evoai.service.ts | 2 +- .../services/evolutionBot.service.ts | 2 +- .../flowise/services/flowise.service.ts | 2 +- .../chatbot/n8n/services/n8n.service.ts | 2 +- .../typebot/controllers/typebot.controller.ts | 41 +- .../typebot/services/typebot.service.ts | 1382 +++++++++++------ 12 files changed, 951 insertions(+), 507 deletions(-) create mode 100644 prisma/postgresql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql diff --git a/prisma/postgresql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql b/prisma/postgresql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql new file mode 100644 index 00000000..5234bd46 --- /dev/null +++ b/prisma/postgresql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "Typebot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, +ADD COLUMN "timePerChar" INTEGER DEFAULT 50; + +-- AlterTable +ALTER TABLE "TypebotSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, +ADD COLUMN "timePerChar" INTEGER DEFAULT 50; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index a0a878df..86f5ae6c 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -357,6 +357,8 @@ model Typebot { triggerType TriggerType? triggerOperator TriggerOperator? triggerValue String? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String TypebotSetting TypebotSetting[] @@ -374,6 +376,8 @@ model TypebotSetting { debounceTime Int? @db.Integer typebotIdFallback String? @db.VarChar(100) ignoreJids Json? + splitMessages Boolean? @default(false) @db.Boolean + timePerChar Int? @default(50) @db.Integer createdAt DateTime? @default(now()) @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id]) diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 34c885d5..4eb644b1 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -165,7 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService { openAiDefaultSettings.speechToText && received?.message?.audioMessage ) { - messageRaw.message.speechToText = await this.openaiService.speechToText(received, this); + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; } } diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 656710e9..add39279 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -520,7 +520,7 @@ export class BusinessStartupService extends ChannelStartupService { openAiDefaultSettings.speechToText ) { try { - messageRaw.message.speechToText = await this.openaiService.speechToText( + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( openAiDefaultSettings.OpenaiCreds, { message: { @@ -528,7 +528,7 @@ export class BusinessStartupService extends ChannelStartupService { ...messageRaw, }, }, - ); + )}`; } catch (speechError) { this.logger.error(`Error processing speech-to-text: ${speechError}`); } @@ -554,7 +554,7 @@ export class BusinessStartupService extends ChannelStartupService { if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { try { - messageRaw.message.speechToText = await this.openaiService.speechToText( + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( openAiDefaultSettings.OpenaiCreds, { message: { @@ -562,7 +562,7 @@ export class BusinessStartupService extends ChannelStartupService { ...messageRaw, }, }, - ); + )}`; } catch (speechError) { this.logger.error(`Error processing speech-to-text: ${speechError}`); } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index bee4bb12..2aaae864 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1188,7 +1188,7 @@ export class BaileysStartupService extends ChannelStartupService { }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = await this.openaiService.speechToText(received, this); + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; } } @@ -2111,7 +2111,7 @@ export class BaileysStartupService extends ChannelStartupService { }); if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - messageRaw.message.speechToText = await this.openaiService.speechToText(messageRaw, this); + messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`; } } diff --git a/src/api/integrations/chatbot/base-chatbot.controller.ts b/src/api/integrations/chatbot/base-chatbot.controller.ts index dab51e25..05a45beb 100644 --- a/src/api/integrations/chatbot/base-chatbot.controller.ts +++ b/src/api/integrations/chatbot/base-chatbot.controller.ts @@ -280,7 +280,7 @@ export abstract class BaseChatbotController { this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`); const transcription = await this.openaiService.speechToText(msg, instance); if (transcription) { - processedContent = transcription; + processedContent = `[audio] ${transcription}`; } } catch (err) { this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`); diff --git a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts index 8f988991..081c2ffc 100644 --- a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts +++ b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts @@ -64,7 +64,7 @@ export class EvolutionBotService extends BaseChatbotService { this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`); const transcription = await this.openaiService.speechToText(msg, instance); if (transcription) { - payload.question = transcription; + payload.question = `[audio] ${transcription}`; } } catch (err) { this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`); diff --git a/src/api/integrations/chatbot/n8n/services/n8n.service.ts b/src/api/integrations/chatbot/n8n/services/n8n.service.ts index 2d37dca0..2d0802b9 100644 --- a/src/api/integrations/chatbot/n8n/services/n8n.service.ts +++ b/src/api/integrations/chatbot/n8n/services/n8n.service.ts @@ -61,7 +61,7 @@ export class N8nService extends BaseChatbotService { this.logger.debug(`[N8n] Downloading audio for Whisper transcription`); const transcription = await this.openaiService.speechToText(msg, instance); if (transcription) { - payload.chatInput = transcription; + payload.chatInput = `[audio] ${transcription}`; } } catch (err) { this.logger.error(`[N8n] Failed to transcribe audio: ${err}`); diff --git a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts index 169850f9..5e1b540f 100644 --- a/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts +++ b/src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts @@ -90,8 +90,25 @@ export class TypebotController extends BaseChatbotController { } /** - * Send a message to the Typebot API + * Base class wrapper - calls the original processTypebot method */ protected async sendMessageToBot( instance: any, @@ -40,490 +42,30 @@ export class TypebotService extends BaseChatbotService { 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: any; - - 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], - }; - } - - if (this.isAudioMessage(content) && msg) { - try { - this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`); - const transcription = await this.openaiService.speechToText(msg, instance); - if (transcription) { - reqData.message = `[audio] ${transcription}`; - } - } catch (err) { - this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`); - } - } - - 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, - ); - } catch (error) { - this.logger.error(`Error in sendMessageToBot for Typebot: ${error.message || JSON.stringify(error)}`); - } + // Map the base class call to the original processTypebot method + await this.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, + ); } /** - * Initialize a new Typebot session + * Simplified wrapper for controller compatibility */ - 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; - let url: string; - let reqData: {}; - - if (version === 'latest') { - url = `${bot.url}/api/v1/typebots/${bot.typebot}/startChat`; - reqData = { - prefilledVariables: { - ...(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, - }, - }; - } else { - url = `${bot.url}/api/v1/sendMessage`; - reqData = { - startParams: { - publicId: bot.typebot, - prefilledVariables: { - ...(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, - }, - }, - }; - } - - const request = await axios.post(url, reqData); - - // Create or update session with the Typebot session ID - let updatedSession = session; - if (request?.data?.sessionId) { - if (session) { - updatedSession = await this.prismaRepository.integrationSession.update({ - where: { id: session.id }, - data: { - sessionId: `${id}-${request.data.sessionId}`, - status: 'opened', - awaitUser: false, - }, - }); - } 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, - ); - } - } catch (error) { - 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: 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, - ) { - if (!messages || messages.length === 0) { - return; - } - - try { - await this.processTypebotMessages(instance, session, settings, remoteJid, messages, input, clientSideActions); - } catch (err) { - this.logger.error(`Error processing Typebot messages: ${err}`); - } - } - - /** - * 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); - }; - - // Helper to apply formatting to message content - const applyFormatting = (element) => { - if (!element) return ''; - - let formattedText = ''; - - 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 || ''; - - // Apply bold formatting - if (item.bold) text = `*${text}*`; - - // Apply italic formatting - if (item.italic) text = `_${text}_`; - - // Apply strikethrough formatting (if supported) - if (item.strikethrough) text = `~${text}~`; - - // Apply URL if present (convert to Markdown style link) - if (item.url) text = `[${text}](${item.url})`; - - return acc + text; - }, ''); - } - - return 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); - - // Skip empty messages - if (!content) continue; - - // Check for WhatsApp list format - const listMatch = content.match(/\[list:(.+?)\]\[(.*?)\]/s); - if (listMatch) { - const { sections, buttonText } = this.parseListFormat(content); - - if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - - // Send as WhatsApp list - // Using instance directly since waMonitor might not have sendListMessage - await instance.sendListMessage({ - number: remoteJid.split('@')[0], - sections, - buttonText, - }); - continue; - } - - // Check for WhatsApp button format - const buttonMatch = content.match(/\[button:(.+?)\]/); - if (buttonMatch) { - const { text, buttons } = this.parseButtonFormat(content); - - 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; - } - - // Process for standard text messages - if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - await this.sendMessageWhatsApp(instance, remoteJid, content, settings); - } - - // Handle image type messages - else if (message.type === 'image') { - const url = message.content?.url || message.content?.imageUrl || ''; - if (!url) continue; - - const caption = message.content?.caption || ''; - const wait = findItemAndGetSecondsToWait(messages, message.id); - - if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - - // Send image to WhatsApp - await instance.sendMediaMessage({ - number: remoteJid.split('@')[0], - type: 'image', - media: url, - caption, - }); - } - - // 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 caption = message.content?.caption || ''; - const wait = findItemAndGetSecondsToWait(messages, message.id); - - if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait * 1000)); - - // Send media to WhatsApp - await instance.sendMediaMessage({ - number: remoteJid.split('@')[0], - type: mediaType, - media: url, - caption, - }); - } - } - - // 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 { - 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: [] }; - } - } - /** - * Simplified method that matches the base class pattern - * This should be the preferred way for the controller to call - */ - public async processTypebot( + public async processTypebotSimple( instance: any, remoteJid: string, bot: TypebotModel, @@ -535,4 +77,880 @@ export class TypebotService extends BaseChatbotService { ): Promise { return this.process(instance, remoteJid, bot, session, settings, content, pushName, msg); } + + /** + * Create a new TypeBot session with prefilled variables + */ + public async createNewSession(instance: Instance, data: any) { + if (data.remoteJid === 'status@broadcast') return; + const id = Math.floor(Math.random() * 10000000000).toString(); + + try { + 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`; + + reqData = { + prefilledVariables: { + ...data.prefilledVariables, + remoteJid: data.remoteJid, + pushName: data.pushName || data.prefilledVariables?.pushName || '', + instanceName: instance.name, + serverUrl: this.configService.get('SERVER').URL, + apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + ownerJid: instance.number, + }, + }; + } else { + url = `${data.url}/api/v1/sendMessage`; + + reqData = { + startParams: { + publicId: data.typebot, + prefilledVariables: { + ...data.prefilledVariables, + remoteJid: data.remoteJid, + pushName: data.pushName || data.prefilledVariables?.pushName || '', + instanceName: instance.name, + 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; + 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, + }, + awaitUser: false, + botId: data.botId, + type: 'typebot', + Instance: { + connect: { + id: instance.id, + }, + }, + }, + }); + } + return { ...request.data, session }; + } catch (error) { + this.logger.error(error); + return; + } + } + + /** + * Send WhatsApp message with complex TypeBot formatting + */ + public async sendWAMessage( + instanceDb: Instance, + 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, + ) { + const waInstance = this.waMonitor.waInstances[instanceDb.name]; + await this.processMessages( + waInstance, + session, + settings, + messages, + input, + clientSideActions, + this.applyFormatting, + this.prismaRepository, + ).catch((err) => { + console.error('Erro ao processar mensagens:', err); + }); + } + + /** + * Apply rich text formatting for TypeBot messages + */ + private applyFormatting(element: any): string { + let text = ''; + + if (element.text) { + text += element.text; + } + + if (element.children && element.type !== 'a') { + for (const child of element.children) { + text += this.applyFormatting(child); + } + } + + if (element.type === 'p' && element.type !== 'inline-variable') { + text = text.trim() + '\n'; + } + + if (element.type === 'inline-variable') { + text = text.trim(); + } + + if (element.type === 'ol') { + text = + '\n' + + text + .split('\n') + .map((line, index) => (line ? `${index + 1}. ${line}` : '')) + .join('\n'); + } + + if (element.type === 'li') { + text = text + .split('\n') + .map((line) => (line ? ` ${line}` : '')) + .join('\n'); + } + + let formats = ''; + + if (element.bold) { + formats += '*'; + } + + 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 formattedText; + } + + /** + * Process TypeBot messages with full feature support + */ + private async 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, + ) { + // Helper function to find wait time + const findItemAndGetSecondsToWait = (array: any[], targetId: string) => { + if (!array) return null; + + for (const item of array) { + if (item.lastBubbleBlockId === targetId) { + return item.wait?.secondsToWaitFor; + } + } + return null; + }; + + for (const message of messages) { + if (message.type === 'text') { + let formattedText = ''; + + for (const richText of message.content.richText) { + for (const element of richText.children) { + formattedText += applyFormatting(element); + } + formattedText += '\n'; + } + + formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, ''); + + formattedText = formattedText.replace(/\n$/, ''); + + if (formattedText.includes('[list]')) { + await this.processListMessage(instance, formattedText, session.remoteJid); + } else if (formattedText.includes('[buttons]')) { + await this.processButtonMessage(instance, formattedText, session.remoteJid); + } else { + await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings); + } + + sendTelemetry('/message/sendText'); + } + + if (message.type === 'image') { + await instance.mediaMessage( + { + number: session.remoteJid.split('@')[0], + delay: settings?.delayMessage || 1000, + mediatype: 'image', + media: message.content.url, + }, + null, + false, + ); + + sendTelemetry('/message/sendMedia'); + } + + if (message.type === 'video') { + await instance.mediaMessage( + { + number: session.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: session.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 input choices + if (input) { + if (input.type === 'choice input') { + let formattedText = ''; + + const items = input.items; + + for (const item of items) { + formattedText += `▶️ ${item.content}\n`; + } + + formattedText = formattedText.replace(/\n$/, ''); + + if (formattedText.includes('[list]')) { + await this.processListMessage(instance, formattedText, session.remoteJid); + } else if (formattedText.includes('[buttons]')) { + await this.processButtonMessage(instance, formattedText, session.remoteJid); + } else { + await this.sendMessageWhatsApp(instance, session.remoteJid, formattedText, settings); + } + + sendTelemetry('/message/sendText'); + } + + await prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + awaitUser: true, + }, + }); + } else { + if (!settings?.keepOpen) { + await prismaRepository.integrationSession.deleteMany({ + where: { + id: session.id, + }, + }); + } else { + await prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'closed', + }, + }); + } + } + } + + /** + * Process list messages for WhatsApp + */ + private async processListMessage(instance: any, formattedText: string, remoteJid: string) { + 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); + } + + /** + * Process button messages for WhatsApp + */ + private async processButtonMessage(instance: any, formattedText: string, remoteJid: string) { + 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); + } + + /** + * Original TypeBot processing method with full functionality + */ + public async processTypebot( + waInstance: any, + remoteJid: string, + msg: Message, + session: IntegrationSession, + findTypebot: TypebotModel, + url: string, + expire: number, + typebot: string, + keywordFinish: string, + delayMessage: number, + unknownMessage: string, + listeningFromMe: boolean, + stopBotFromMe: boolean, + keepOpen: boolean, + content: string, + prefilledVariables?: any, + ) { + // Get the database instance record + const instance = await this.prismaRepository.instance.findFirst({ + where: { + name: waInstance.instanceName, + }, + }); + + if (!instance) { + this.logger.error('Instance not found in database'); + return; + } + // Handle session expiration + 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 || data.messages.length === 0) { + const content = getConversationMessage(msg.message); + + if (!content) { + if (unknownMessage) { + await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { + delayMessage, + expire, + keywordFinish, + listeningFromMe, + stopBotFromMe, + keepOpen, + unknownMessage, + }); + 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; + } + } + + if (data?.messages && data.messages.length > 0) { + 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.status !== 'opened') { + return; + } + + // Handle new sessions + 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; + } + + if (data?.messages && data.messages.length > 0) { + 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 || data.messages.length === 0) { + if (!content) { + if (unknownMessage) { + await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { + delayMessage, + expire, + keywordFinish, + listeningFromMe, + stopBotFromMe, + keepOpen, + unknownMessage, + }); + 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; + } + + // Update existing session + await this.prismaRepository.integrationSession.update({ + where: { + id: session.id, + }, + data: { + status: 'opened', + awaitUser: false, + }, + }); + + if (!content) { + if (unknownMessage) { + await this.sendMessageWhatsApp(waInstance, remoteJid, unknownMessage, { + delayMessage, + expire, + keywordFinish, + listeningFromMe, + stopBotFromMe, + keepOpen, + unknownMessage, + }); + 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; + } + + // Continue existing chat + const version = this.configService.get('TYPEBOT').API_VERSION; + let urlTypebot: string; + let reqData: { message: string; sessionId?: string }; + 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], + }; + } + + // Handle audio transcription if OpenAI service is available + if (this.isAudioMessage(content) && msg) { + try { + this.logger.debug(`[TypeBot] Downloading audio for Whisper transcription`); + const transcription = await this.openaiService.speechToText(msg, instance); + if (transcription) { + reqData.message = `[audio] ${transcription}`; + } + } catch (err) { + this.logger.error(`[TypeBot] Failed to transcribe audio: ${err}`); + } + } + + 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; + } }