Files
evolution-api/src/api/integrations/chatbot/base-chatbot.service.ts
OrionDesign bbf142cf39 fix: corrige comportamento de sessão pausada no Evolution Bot
Corrige o problema onde o Evolution Bot reativava automaticamente por qualquer mensagem do usuario quando a sessão estava pausada. Agora, quando uma sessão está pausada, o bot ignora completamente as mensagens recebidas até que a sessão seja explicitamente reativada.
2025-05-22 01:01:51 -03:00

408 lines
12 KiB
TypeScript

import { InstanceDto } from '@api/dto/instance.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Integration } from '@api/types/wa.types';
import { ConfigService } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { IntegrationSession } from '@prisma/client';
/**
* Base class for all chatbot service implementations
* Contains common methods shared across different chatbot integrations
*/
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
protected readonly logger: Logger;
protected readonly waMonitor: WAMonitoringService;
protected readonly prismaRepository: PrismaRepository;
protected readonly configService?: ConfigService;
constructor(
waMonitor: WAMonitoringService,
prismaRepository: PrismaRepository,
loggerName: string,
configService?: ConfigService,
) {
this.waMonitor = waMonitor;
this.prismaRepository = prismaRepository;
this.logger = new Logger(loggerName);
this.configService = configService;
}
/**
* Check if a message contains an image
*/
protected isImageMessage(content: string): boolean {
return content.includes('imageMessage');
}
/**
* Check if a message contains audio
*/
protected isAudioMessage(content: string): boolean {
return content.includes('audioMessage');
}
/**
* Check if a string is valid JSON
*/
protected isJSON(str: string): boolean {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
/**
* Determine the media type from a URL based on its extension
*/
protected getMediaType(url: string): string | null {
const extension = url.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
if (imageExtensions.includes(extension || '')) return 'image';
if (audioExtensions.includes(extension || '')) return 'audio';
if (videoExtensions.includes(extension || '')) return 'video';
if (documentExtensions.includes(extension || '')) return 'document';
return null;
}
/**
* Create a new chatbot session
*/
public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
try {
// Extract pushName safely - if data.pushName is an object with a pushName property, use that
const pushNameValue =
typeof data.pushName === 'object' && data.pushName?.pushName
? data.pushName.pushName
: typeof data.pushName === 'string'
? data.pushName
: null;
// Extract remoteJid safely
const remoteJidValue =
typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;
const session = await this.prismaRepository.integrationSession.create({
data: {
remoteJid: remoteJidValue,
pushName: pushNameValue,
sessionId: remoteJidValue,
status: 'opened',
awaitUser: false,
botId: data.botId,
instanceId: instance.instanceId,
type: type,
},
});
return { session };
} catch (error) {
this.logger.error(error);
return;
}
}
/**
* Standard implementation for processing incoming messages
* This handles the common workflow across all chatbot types:
* 1. Check for existing session or create new one
* 2. Handle message based on session state
*/
public async process(
instance: any,
remoteJid: string,
bot: BotType,
session: IntegrationSession,
settings: SettingsType,
content: string,
pushName?: string,
msg?: any,
): Promise<void> {
try {
// For new sessions or sessions awaiting initialization
if (!session) {
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
return;
}
// If session is paused, ignore the message
if (session.status === 'paused') {
return;
}
// For existing sessions, keywords might indicate the conversation should end
const keywordFinish = (settings as any)?.keywordFinish || '';
const normalizedContent = content.toLowerCase().trim();
if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
// Update session to closed and return
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'closed',
},
});
return;
}
// Forward the message to the chatbot API
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
// 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;
}
}
/**
* Standard implementation for sending messages to WhatsApp
* This handles common patterns like markdown links and formatting
*/
protected async sendMessageWhatsApp(
instance: any,
remoteJid: string,
message: string,
settings: SettingsType,
): Promise<void> {
if (!message) return;
const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
let textBuffer = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const splitMessages = (settings as any)?.splitMessages ?? false;
while ((match = linkRegex.exec(message)) !== null) {
const [fullMatch, altText, url] = match;
const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index);
if (beforeText) {
textBuffer += beforeText;
}
if (mediaType) {
// Send accumulated text before sending media
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
textBuffer = '';
}
// Handle sending the media
try {
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,
fileName: mediaType === 'document' ? altText || 'document' : undefined,
},
null,
false,
);
}
} catch (error) {
this.logger.error(`Error sending media: ${error}`);
// If media fails, at least send the alt text and URL
textBuffer += `${altText}: ${url}`;
}
} else {
// It's a regular link, keep it in the text
textBuffer += fullMatch;
}
lastIndex = linkRegex.lastIndex;
}
// Add any remaining text after the last match
if (lastIndex < message.length) {
const remainingText = message.slice(lastIndex);
if (remainingText.trim()) {
textBuffer += remainingText;
}
}
// Send any remaining text
if (textBuffer.trim()) {
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
}
}
/**
* Helper method to send formatted text with proper typing indicators and delays
*/
private async sendFormattedText(
instance: any,
remoteJid: string,
text: string,
settings: any,
splitMessages: boolean,
): Promise<void> {
const timePerChar = settings?.timePerChar ?? 0;
const minDelay = 1000;
const maxDelay = 20000;
if (splitMessages) {
const multipleMessages = text.split('\n\n');
for (let index = 0; index < multipleMessages.length; index++) {
const message = multipleMessages[index];
if (!message.trim()) continue;
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: message,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
} else {
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.presenceSubscribe(remoteJid);
await instance.client.sendPresenceUpdate('composing', remoteJid);
}
await new Promise<void>((resolve) => {
setTimeout(async () => {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: text,
},
false,
);
resolve();
}, delay);
});
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
await instance.client.sendPresenceUpdate('paused', remoteJid);
}
}
}
/**
* Standard implementation for initializing a new session
* This method should be overridden if a subclass needs specific initialization
*/
protected async initNewSession(
instance: any,
remoteJid: string,
bot: BotType,
settings: SettingsType,
session: IntegrationSession,
content: string,
pushName?: string | any,
msg?: any,
): Promise<void> {
// Create a session if none exists
if (!session) {
// Extract pushName properly - if it's an object with pushName property, use that
const pushNameValue =
typeof pushName === 'object' && pushName?.pushName
? pushName.pushName
: typeof pushName === 'string'
? pushName
: null;
session = (
await this.createNewSession(
{
instanceName: instance.instanceName,
instanceId: instance.instanceId,
},
{
remoteJid,
pushName: pushNameValue,
botId: (bot as any).id,
},
this.getBotType(),
)
)?.session;
}
// Update session status to opened
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: false,
},
});
// Forward the message to the chatbot
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
}
/**
* Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
* This should match the type field used in the IntegrationSession
*/
protected abstract getBotType(): string;
/**
* Send a message to the chatbot API
* This is specific to each chatbot integration
*/
protected abstract sendMessageToBot(
instance: any,
session: IntegrationSession,
settings: SettingsType,
bot: BotType,
remoteJid: string,
pushName: string,
content: string,
msg?: any,
): Promise<void>;
}