From 2982958c058a1e5acd16b4219eb79cbc7d466a14 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 12 Jan 2025 12:49:50 -0500 Subject: [PATCH 1/3] Criando WebWidget --- Dockerfile.dev | 16 + docker-compose.dev.yaml | 64 +- src/api/dto/sendMessage.dto.ts | 11 +- .../evolution/evolution.channel.service.ts | 567 +++++--- .../channel/evolution/evolution.controller.ts | 67 +- .../channel/evolution/evolution.router.ts | 62 +- .../controllers/chatwoot.controller.ts | 60 +- .../chatwoot/services/chatwoot.service.ts | 1002 +++++++++----- .../openai/controllers/openai.controller.ts | 1172 ++++++++++------- .../chatbot/openai/services/openai.service.ts | 209 ++- src/api/services/channel.service.ts | 206 +-- src/config/logger.config.ts | 4 +- 12 files changed, 2313 insertions(+), 1127 deletions(-) create mode 100644 Dockerfile.dev diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..a3d1d653 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,16 @@ +FROM node:20-slim + +WORKDIR /evolution + +RUN apt-get update && apt-get install -y python3 make g++ libvips-dev git pkg-config +RUN npm install --platform=linux --arch=x64 sharp +COPY package*.json /evolution/ +RUN npm install + +COPY . /evolution + +# Generate your Prisma Client +# RUN npm run db:deploy +# RUN npx prisma generate --schema=/evolution/prisma/postgresql-schema.prisma + +CMD ["npm", "run", "dev:server"] \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 2ca3424e..f232bcaf 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,25 +1,57 @@ services: - api: - container_name: evolution_api - image: evolution/api:local - build: . - restart: always - ports: - - 8080:8080 + evolution-api-dev: + container_name: evolution-api-dev + build: + context: . + dockerfile: Dockerfile.dev + depends_on: + - evolution-redis + - evolution-postgres volumes: - - evolution_instances:/evolution/instances + - type: bind + source: . + target: /evolution + - type: volume + target: /evolution/node_modules + ports: + - '8080:8080' + environment: + - NODE_ENV=development networks: - - evolution-net + - chatwoot-evolution-network env_file: - - .env - expose: - - 8080 + - ./.env + + evolution-redis: + image: redis:latest + container_name: evolution-redis + command: > + redis-server --port 6380 --appendonly yes + volumes: + - evolution-redis:/data + networks: + - chatwoot-evolution-network + + evolution-postgres: + container_name: evolution-postgres + image: postgres:15 + command: [ "postgres", "-c", "max_connections=1000" ] + restart: always + volumes: + - evolution-postgres:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + networks: + - chatwoot-evolution-network volumes: - evolution_instances: + evolution-instances: + evolution-redis: + evolution-postgres: networks: - evolution-net: - name: evolution-net - driver: bridge + chatwoot-evolution-network: + external: true diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index 1c9b1154..801db0fe 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -47,8 +47,17 @@ export class Metadata { } export class SendTextDto extends Metadata { - text: string; + text: string; // Conteúdo do texto + number: string; // WhatsApp ou 'webwidget:...' + channel?: string; // ex: 'Channel::WebWidget' ou outro + inbox_id?: number; // se quiser mandar explicitamente ID=3 + delay?: number; + quoted?: any; + linkPreview?: boolean; + mentionsEveryOne?: boolean; + mentioned?: string[]; } + export class SendPresence extends Metadata { text: string; } diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 10b28a4d..88c071ce 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -1,4 +1,10 @@ -import { MediaMessage, Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto'; +import { + MediaMessage, + Options, + SendAudioDto, + SendMediaDto, + SendTextDto, +} from '@api/dto/sendMessage.dto'; import { ProviderFiles } from '@api/provider/sessions'; import { PrismaRepository } from '@api/repository/repository.service'; import { chatbotController } from '@api/server.module'; @@ -6,7 +12,10 @@ import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; import { Events, wa } from '@api/types/wa.types'; import { Chatwoot, ConfigService, Openai } from '@config/env.config'; -import { BadRequestException, InternalServerErrorException } from '@exceptions'; +import { + BadRequestException, + InternalServerErrorException, +} from '@exceptions'; import { status } from '@utils/renderStatus'; import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; @@ -36,14 +45,25 @@ export class EvolutionStartupService extends ChannelStartupService { public mobile: boolean; public get connectionStatus() { + this.logger.log('[connectionStatus] Retornando estado da conexão'); return this.stateConnection; } public async closeClient() { - this.stateConnection = { state: 'close' }; + this.logger.log('[closeClient] Encerrando cliente...'); + try { + this.stateConnection = { state: 'close' }; + this.logger.debug('[closeClient] stateConnection atualizado para "close"'); + } catch (error) { + this.logger.error( + `[closeClient] Erro ao tentar fechar o cliente: ${error?.toString()}`, + ); + throw new InternalServerErrorException(error?.toString()); + } } public get qrCode(): wa.QrCode { + this.logger.log('[qrCode] Obtendo informações do QR Code...'); return { pairingCode: this.instance.qrcode?.pairingCode, code: this.instance.qrcode?.code, @@ -53,10 +73,14 @@ export class EvolutionStartupService extends ChannelStartupService { } public async logoutInstance() { + this.logger.log('[logoutInstance] Realizando logout da instância...'); await this.closeClient(); } public async profilePicture(number: string) { + this.logger.log( + `[profilePicture] Obtendo foto de perfil para o número: ${number}`, + ); const jid = this.createJid(number); return { @@ -66,35 +90,50 @@ export class EvolutionStartupService extends ChannelStartupService { } public async getProfileName() { + this.logger.log('[getProfileName] Método não implementado...'); return null; } public async profilePictureUrl() { + this.logger.log('[profilePictureUrl] Método não implementado...'); return null; } public async getProfileStatus() { + this.logger.log('[getProfileStatus] Método não implementado...'); return null; } public async connectToWhatsapp(data?: any): Promise { - if (!data) return; + this.logger.log('[connectToWhatsapp] Iniciando conexão com o Whatsapp...'); + if (!data) { + this.logger.warn('[connectToWhatsapp] Nenhum dado recebido. Encerrando...'); + return; + } try { + this.logger.debug('[connectToWhatsapp] Carregando Chatwoot...'); this.loadChatwoot(); + this.logger.debug('[connectToWhatsapp] Chamando eventHandler...'); this.eventHandler(data); } catch (error) { - this.logger.error(error); + this.logger.error( + `[connectToWhatsapp] Erro ao conectar ao Whatsapp: ${error?.toString()}`, + ); throw new InternalServerErrorException(error?.toString()); } } protected async eventHandler(received: any) { + this.logger.log('[eventHandler] Iniciando tratamento de evento...'); try { let messageRaw: any; if (received.message) { + this.logger.debug( + `[eventHandler] Mensagem recebida: ${JSON.stringify(received)}`, + ); const key = { id: received.key.id || v4(), remoteJid: received.key.remoteJid, @@ -110,15 +149,26 @@ export class EvolutionStartupService extends ChannelStartupService { instanceId: this.instanceId, }; + this.logger.debug( + `[eventHandler] Montando objeto messageRaw: ${JSON.stringify( + messageRaw, + )}`, + ); + + // Verifica OpenAI if (this.configService.get('OPENAI').ENABLED) { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: this.instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); + this.logger.debug( + '[eventHandler] Verificando configurações do OpenAI...', + ); + const openAiDefaultSettings = + await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: this.instanceId, + }, + include: { + OpenaiCreds: true, + }, + }); if ( openAiDefaultSettings && @@ -126,6 +176,9 @@ export class EvolutionStartupService extends ChannelStartupService { openAiDefaultSettings.speechToText && received?.message?.audioMessage ) { + this.logger.debug( + '[eventHandler] Realizando speech-to-text no áudio...', + ); messageRaw.message.speechToText = await this.openaiService.speechToText( openAiDefaultSettings.OpenaiCreds, received, @@ -134,52 +187,113 @@ export class EvolutionStartupService extends ChannelStartupService { } } - this.logger.log(messageRaw); + this.logger.log(`[eventHandler] messageRaw final: ${JSON.stringify(messageRaw)}`); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + this.logger.debug('[eventHandler] Emitindo chatbotController...'); await chatbotController.emit({ - instance: { instanceName: this.instance.name, instanceId: this.instanceId }, + instance: { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, }); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { + this.logger.debug('[eventHandler] Enviando evento para Chatwoot...'); const chatwootSentMessage = await this.chatwootService.eventWhatsapp( Events.MESSAGES_UPSERT, - { instanceName: this.instance.name, instanceId: this.instanceId }, + { + instanceName: this.instance.name, + instanceId: this.instanceId, + }, messageRaw, ); if (chatwootSentMessage?.id) { + this.logger.debug( + `[eventHandler] chatwootSentMessage criado com ID: ${chatwootSentMessage.id}`, + ); messageRaw.chatwootMessageId = chatwootSentMessage.id; messageRaw.chatwootInboxId = chatwootSentMessage.id; messageRaw.chatwootConversationId = chatwootSentMessage.id; } } + this.logger.debug('[eventHandler] Salvando mensagem no Prisma...'); await this.prismaRepository.message.create({ data: messageRaw, }); + this.logger.debug('[eventHandler] Atualizando contato...'); await this.updateContact({ remoteJid: messageRaw.key.remoteJid, - pushName: messageRaw.key.fromMe ? '' : messageRaw.key.fromMe == null ? '' : received.pushName, + pushName: messageRaw.key.fromMe + ? '' + : messageRaw.key.fromMe == null + ? '' + : received.pushName, profilePicUrl: received.profilePicUrl, }); } } catch (error) { - this.logger.error(error); + this.logger.error(`[eventHandler] Erro: ${error}`); } } - private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) { - const contact = await this.prismaRepository.contact.findFirst({ - where: { instanceId: this.instanceId, remoteJid: data.remoteJid }, - }); + private async updateContact(data: { + remoteJid: string; + pushName?: string; + profilePicUrl?: string; + }) { + this.logger.log( + `[updateContact] Atualizando ou criando contato para: ${data.remoteJid}`, + ); + try { + const contact = await this.prismaRepository.contact.findFirst({ + where: { instanceId: this.instanceId, remoteJid: data.remoteJid }, + }); - if (contact) { + if (contact) { + this.logger.debug( + `[updateContact] Contato já existe. Atualizando...: ${contact.remoteJid}`, + ); + const contactRaw: any = { + remoteJid: data.remoteJid, + pushName: data?.pushName, + instanceId: this.instanceId, + profilePicUrl: data?.profilePicUrl, + }; + + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled + ) { + this.logger.debug('[updateContact] Atualizando contato no Chatwoot...'); + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name, instanceId: this.instanceId }, + contactRaw, + ); + } + + this.logger.debug('[updateContact] Atualizando contato no Prisma...'); + await this.prismaRepository.contact.updateMany({ + where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, + data: contactRaw, + }); + return; + } + + this.logger.debug('[updateContact] Contato não encontrado. Criando novo...'); const contactRaw: any = { remoteJid: data.remoteJid, pushName: data?.pushName, @@ -187,103 +301,115 @@ export class EvolutionStartupService extends ChannelStartupService { profilePicUrl: data?.profilePicUrl, }; - this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name, instanceId: this.instanceId }, - contactRaw, - ); - } - - await this.prismaRepository.contact.updateMany({ - where: { remoteJid: contact.remoteJid, instanceId: this.instanceId }, + await this.prismaRepository.contact.create({ data: contactRaw, }); - return; - } - const contactRaw: any = { - remoteJid: data.remoteJid, - pushName: data?.pushName, - instanceId: this.instanceId, - profilePicUrl: data?.profilePicUrl, - }; - - this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - await this.prismaRepository.contact.create({ - data: contactRaw, - }); - - const chat = await this.prismaRepository.chat.findFirst({ - where: { instanceId: this.instanceId, remoteJid: data.remoteJid }, - }); - - if (chat) { - const chatRaw: any = { - remoteJid: data.remoteJid, - instanceId: this.instanceId, - }; - - this.sendDataWebhook(Events.CHATS_UPDATE, chatRaw); - - await this.prismaRepository.chat.updateMany({ - where: { remoteJid: chat.remoteJid }, - data: chatRaw, + const chat = await this.prismaRepository.chat.findFirst({ + where: { instanceId: this.instanceId, remoteJid: data.remoteJid }, }); + + if (chat) { + this.logger.debug( + `[updateContact] Chat já existe para este contato. Atualizando...: ${chat.remoteJid}`, + ); + const chatRaw: any = { + remoteJid: data.remoteJid, + instanceId: this.instanceId, + }; + + this.sendDataWebhook(Events.CHATS_UPDATE, chatRaw); + + await this.prismaRepository.chat.updateMany({ + where: { remoteJid: chat.remoteJid }, + data: chatRaw, + }); + } else { + this.logger.debug( + '[updateContact] Nenhum chat encontrado para este contato. Criando novo...', + ); + const chatRaw: any = { + remoteJid: data.remoteJid, + instanceId: this.instanceId, + }; + + this.sendDataWebhook(Events.CHATS_UPSERT, chatRaw); + + await this.prismaRepository.chat.create({ + data: chatRaw, + }); + } + } catch (error) { + this.logger.error(`[updateContact] Erro ao atualizar/criar contato: ${error}`); } - - const chatRaw: any = { - remoteJid: data.remoteJid, - instanceId: this.instanceId, - }; - - this.sendDataWebhook(Events.CHATS_UPSERT, chatRaw); - - await this.prismaRepository.chat.create({ - data: chatRaw, - }); } - protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) { + protected async sendMessageWithTyping( + number: string, + message: any, + options?: Options, + isIntegration = false, + ) { + this.logger.log(`[sendMessageWithTyping] Enviando mensagem para: ${number}`); + this.logger.debug( + `[sendMessageWithTyping] Mensagem: ${JSON.stringify(message)}, Options: ${JSON.stringify( + options, + )}, isIntegration: ${isIntegration}`, + ); try { let quoted: any; let webhookUrl: any; if (options?.quoted) { + this.logger.debug('[sendMessageWithTyping] Opção quoted detectada...'); const m = options?.quoted; - const msg = m?.key; if (!msg) { + this.logger.error('[sendMessageWithTyping] Mensagem de citação não encontrada!'); throw 'Message not found'; } quoted = msg; } - if (options.delay) { + if (options?.delay) { + this.logger.debug(`[sendMessageWithTyping] Aguardando delay de ${options.delay}ms...`); await new Promise((resolve) => setTimeout(resolve, options.delay)); } if (options?.webhookUrl) { + this.logger.debug( + `[sendMessageWithTyping] Usando webhookUrl customizado: ${options.webhookUrl}`, + ); webhookUrl = options.webhookUrl; } const messageId = v4(); + this.logger.debug( + `[sendMessageWithTyping] Gerando UUID para mensagem: ${messageId}`, + ); + // debug message + this.logger.debug( + `[sendMessageWithTyping] Mensagem a ser enviada: ${JSON.stringify(message)}`, + ); let messageRaw: any = { - key: { fromMe: true, id: messageId, remoteJid: number }, + key: { fromMe: true, id: messageId, remoteJid: number, channel: message.channel, inbox_id: message.inbox_id }, messageTimestamp: Math.round(new Date().getTime() / 1000), webhookUrl, source: 'unknown', instanceId: this.instanceId, status: status[1], }; + // debug messageRaw + this.logger.debug(`[sendMessageWithTyping] messageRaw a ser enviada: ${JSON.stringify(messageRaw)}`); + // Verifica o tipo de mídia para compor a mensagem if (message?.mediaType === 'image') { + this.logger.debug('[sendMessageWithTyping] Montando mensagem de imagem...'); messageRaw = { ...messageRaw, message: { @@ -293,6 +419,7 @@ export class EvolutionStartupService extends ChannelStartupService { messageType: 'imageMessage', }; } else if (message?.mediaType === 'video') { + this.logger.debug('[sendMessageWithTyping] Montando mensagem de vídeo...'); messageRaw = { ...messageRaw, message: { @@ -302,6 +429,7 @@ export class EvolutionStartupService extends ChannelStartupService { messageType: 'videoMessage', }; } else if (message?.mediaType === 'audio') { + this.logger.debug('[sendMessageWithTyping] Montando mensagem de áudio...'); messageRaw = { ...messageRaw, message: { @@ -311,6 +439,7 @@ export class EvolutionStartupService extends ChannelStartupService { messageType: 'audioMessage', }; } else if (message?.mediaType === 'document') { + this.logger.debug('[sendMessageWithTyping] Montando mensagem de documento...'); messageRaw = { ...messageRaw, message: { @@ -320,6 +449,7 @@ export class EvolutionStartupService extends ChannelStartupService { messageType: 'documentMessage', }; } else { + this.logger.debug('[sendMessageWithTyping] Montando mensagem de texto...'); messageRaw = { ...messageRaw, message: { @@ -330,11 +460,21 @@ export class EvolutionStartupService extends ChannelStartupService { }; } - this.logger.log(messageRaw); + this.logger.log( + `[sendMessageWithTyping] messageRaw final: ${JSON.stringify(messageRaw)}`, + ); this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + // debug a proxima funcao + this.logger.debug(`[sendMessageWithTyping] CHATWOOT: ${this.configService.get('CHATWOOT').ENABLED}, LOCAL: ${this.localChatwoot?.enabled}, INTEGRATION: ${isIntegration}`); - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) { + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled && + !isIntegration + ) { + this.logger.debug('[sendMessageWithTyping] Enviando evento SEND_MESSAGE ao Chatwoot...'); this.chatwootService.eventWhatsapp( Events.SEND_MESSAGE, { instanceName: this.instance.name, instanceId: this.instanceId }, @@ -342,38 +482,54 @@ export class EvolutionStartupService extends ChannelStartupService { ); } - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration) + if ( + this.configService.get('CHATWOOT').ENABLED && + this.localChatwoot?.enabled && + isIntegration + ) { + this.logger.debug( + '[sendMessageWithTyping] Emitindo mensagem para chatbotController em modo de integração...', + ); await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, remoteJid: messageRaw.key.remoteJid, msg: messageRaw, pushName: messageRaw.pushName, }); + } + this.logger.debug('[sendMessageWithTyping] Salvando mensagem no Prisma...'); await this.prismaRepository.message.create({ data: messageRaw, }); return messageRaw; } catch (error) { - this.logger.error(error); + this.logger.error( + `[sendMessageWithTyping] Erro ao enviar mensagem para ${number}: ${error}`, + ); throw new BadRequestException(error.toString()); } } - public async textMessage(data: SendTextDto, isIntegration = false) { + public async textMessage(data2: SendTextDto, isIntegration = false) { + this.logger.log('[textMessage] Enviando mensagem de texto...'); + this.logger.debug(`[textMessage] Dados recebidos: ${JSON.stringify(data2)}`); + const res = await this.sendMessageWithTyping( - data.number, + data2.number, { - conversation: data.text, + conversation: data2.text, + channel: data2.channel, // passa channel aqui + inbox_id: data2.inbox_id, // e inbox_id aqui }, { - delay: data?.delay, + delay: data2?.delay, presence: 'composing', - quoted: data?.quoted, - linkPreview: data?.linkPreview, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, + quoted: data2?.quoted, + linkPreview: data2?.linkPreview, + mentionsEveryOne: data2?.mentionsEveryOne, + mentioned: data2?.mentioned, }, isIntegration, ); @@ -381,18 +537,31 @@ export class EvolutionStartupService extends ChannelStartupService { } protected async prepareMediaMessage(mediaMessage: MediaMessage) { + this.logger.log('[prepareMediaMessage] Preparando mensagem de mídia...'); + this.logger.debug( + `[prepareMediaMessage] Dados recebidos: ${JSON.stringify(mediaMessage)}`, + ); try { if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.debug( + '[prepareMediaMessage] Definindo filename para documento...', + ); const regex = new RegExp(/.*\/(.+?)\./); const arrayMatch = regex.exec(mediaMessage.media); mediaMessage.fileName = arrayMatch[1]; } if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { + this.logger.debug( + '[prepareMediaMessage] Definindo filename padrão para imagem...', + ); mediaMessage.fileName = 'image.png'; } if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { + this.logger.debug( + '[prepareMediaMessage] Definindo filename padrão para vídeo...', + ); mediaMessage.fileName = 'video.mp4'; } @@ -406,6 +575,7 @@ export class EvolutionStartupService extends ChannelStartupService { gifPlayback: false, }; + this.logger.debug('[prepareMediaMessage] Verificando mimetype...'); if (isURL(mediaMessage.media)) { mimetype = mime.getType(mediaMessage.media); } else { @@ -414,234 +584,329 @@ export class EvolutionStartupService extends ChannelStartupService { prepareMedia.mimetype = mimetype; + this.logger.debug( + `[prepareMediaMessage] Retornando objeto de mídia preparado: ${JSON.stringify( + prepareMedia, + )}`, + ); return prepareMedia; } catch (error) { - this.logger.error(error); + this.logger.error( + `[prepareMediaMessage] Erro ao preparar mensagem de mídia: ${error}`, + ); throw new InternalServerErrorException(error?.toString() || error); } } public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) { - const mediaData: SendMediaDto = { ...data }; + this.logger.log('[mediaMessage] Enviando mensagem de mídia...'); + this.logger.debug(`[mediaMessage] Dados recebidos: ${JSON.stringify(data)}`); + try { + const mediaData: SendMediaDto = { ...data }; - if (file) mediaData.media = file.buffer.toString('base64'); + if (file) { + this.logger.debug( + '[mediaMessage] Convertendo arquivo em base64 para envio...', + ); + mediaData.media = file.buffer.toString('base64'); + } - const message = await this.prepareMediaMessage(mediaData); + const message = await this.prepareMediaMessage(mediaData); - const mediaSent = await this.sendMessageWithTyping( - data.number, - { ...message }, - { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - linkPreview: data?.linkPreview, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }, - isIntegration, - ); + const mediaSent = await this.sendMessageWithTyping( + data.number, + { ...message }, + { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + linkPreview: data?.linkPreview, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }, + isIntegration, + ); - return mediaSent; + return mediaSent; + } catch (error) { + this.logger.error( + `[mediaMessage] Erro ao enviar mensagem de mídia: ${error}`, + ); + throw new InternalServerErrorException(error?.toString()); + } } public async processAudio(audio: string, number: string) { - number = number.replace(/\D/g, ''); - const hash = `${number}-${new Date().getTime()}`; + this.logger.log('[processAudio] Processando áudio...'); + this.logger.debug(`[processAudio] Áudio: ${audio}, Número: ${number}`); + try { + number = number.replace(/\D/g, ''); + this.logger.debug(`[processAudio] Número formatado: ${number}`); - let mimetype: string; + const hash = `${number}-${new Date().getTime()}`; + let mimetype: string; - const prepareMedia: any = { - fileName: `${hash}.mp4`, - mediaType: 'audio', - media: audio, - }; + const prepareMedia: any = { + fileName: `${hash}.mp4`, + mediaType: 'audio', + media: audio, + }; - if (isURL(audio)) { - mimetype = mime.getType(audio); - } else { - mimetype = mime.getType(prepareMedia.fileName); + if (isURL(audio)) { + mimetype = mime.getType(audio); + } else { + mimetype = mime.getType(prepareMedia.fileName); + } + + prepareMedia.mimetype = mimetype; + this.logger.debug( + `[processAudio] Retornando objeto de mídia de áudio: ${JSON.stringify( + prepareMedia, + )}`, + ); + + return prepareMedia; + } catch (error) { + this.logger.error( + `[processAudio] Erro ao processar áudio: ${error.toString()}`, + ); + throw new InternalServerErrorException(error?.toString()); } - - prepareMedia.mimetype = mimetype; - - return prepareMedia; } public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { - const mediaData: SendAudioDto = { ...data }; + this.logger.log('[audioWhatsapp] Enviando áudio via Whatsapp...'); + this.logger.debug(`[audioWhatsapp] Dados recebidos: ${JSON.stringify(data)}`); + try { + const mediaData: SendAudioDto = { ...data }; - if (file?.buffer) { - mediaData.audio = file.buffer.toString('base64'); - } else { - console.error('El archivo o buffer no est� definido correctamente.'); - throw new Error('File or buffer is undefined.'); + if (file?.buffer) { + this.logger.debug('[audioWhatsapp] Convertendo buffer em base64...'); + mediaData.audio = file.buffer.toString('base64'); + } else { + this.logger.error( + '[audioWhatsapp] O arquivo ou buffer não está definido corretamente.', + ); + throw new Error('File or buffer is undefined.'); + } + + const message = await this.processAudio(mediaData.audio, data.number); + + const audioSent = await this.sendMessageWithTyping( + data.number, + { ...message }, + { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + linkPreview: data?.linkPreview, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }, + isIntegration, + ); + + return audioSent; + } catch (error) { + this.logger.error(`[audioWhatsapp] Erro ao enviar áudio: ${error}`); + throw new InternalServerErrorException(error?.toString()); } - - const message = await this.processAudio(mediaData.audio, data.number); - - const audioSent = await this.sendMessageWithTyping( - data.number, - { ...message }, - { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - linkPreview: data?.linkPreview, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }, - isIntegration, - ); - - return audioSent; } public async buttonMessage() { + this.logger.warn('[buttonMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async locationMessage() { + this.logger.warn('[locationMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async listMessage() { + this.logger.warn('[listMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async templateMessage() { + this.logger.warn('[templateMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async contactMessage() { + this.logger.warn('[contactMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async reactionMessage() { + this.logger.warn('[reactionMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async getBase64FromMediaMessage() { + this.logger.warn('[getBase64FromMediaMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async deleteMessage() { + this.logger.warn('[deleteMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async mediaSticker() { + this.logger.warn('[mediaSticker] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async pollMessage() { + this.logger.warn('[pollMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async statusMessage() { + this.logger.warn('[statusMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async reloadConnection() { + this.logger.warn('[reloadConnection] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async whatsappNumber() { + this.logger.warn('[whatsappNumber] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async markMessageAsRead() { + this.logger.warn('[markMessageAsRead] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async archiveChat() { + this.logger.warn('[archiveChat] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async markChatUnread() { + this.logger.warn('[markChatUnread] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fetchProfile() { + this.logger.warn('[fetchProfile] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async offerCall() { + this.logger.warn('[offerCall] Método não disponível no WhatsApp Business API'); throw new BadRequestException('Method not available on WhatsApp Business API'); } public async sendPresence() { + this.logger.warn('[sendPresence] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async setPresence() { + this.logger.warn('[setPresence] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fetchPrivacySettings() { + this.logger.warn('[fetchPrivacySettings] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updatePrivacySettings() { + this.logger.warn('[updatePrivacySettings] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fetchBusinessProfile() { + this.logger.warn('[fetchBusinessProfile] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateProfileName() { + this.logger.warn('[updateProfileName] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateProfileStatus() { + this.logger.warn('[updateProfileStatus] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateProfilePicture() { + this.logger.warn('[updateProfilePicture] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async removeProfilePicture() { + this.logger.warn('[removeProfilePicture] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async blockUser() { + this.logger.warn('[blockUser] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateMessage() { + this.logger.warn('[updateMessage] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async createGroup() { + this.logger.warn('[createGroup] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateGroupPicture() { + this.logger.warn('[updateGroupPicture] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateGroupSubject() { + this.logger.warn('[updateGroupSubject] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateGroupDescription() { + this.logger.warn('[updateGroupDescription] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async findGroup() { + this.logger.warn('[findGroup] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fetchAllGroups() { + this.logger.warn('[fetchAllGroups] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async inviteCode() { + this.logger.warn('[inviteCode] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async inviteInfo() { + this.logger.warn('[inviteInfo] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async sendInvite() { + this.logger.warn('[sendInvite] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async acceptInviteCode() { + this.logger.warn('[acceptInviteCode] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async revokeInviteCode() { + this.logger.warn('[revokeInviteCode] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async findParticipants() { + this.logger.warn('[findParticipants] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateGParticipant() { + this.logger.warn('[updateGParticipant] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async updateGSetting() { + this.logger.warn('[updateGSetting] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async toggleEphemeral() { + this.logger.warn('[toggleEphemeral] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async leaveGroup() { + this.logger.warn('[leaveGroup] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fetchLabels() { + this.logger.warn('[fetchLabels] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async handleLabel() { + this.logger.warn('[handleLabel] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async receiveMobileCode() { + this.logger.warn('[receiveMobileCode] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } public async fakeCall() { + this.logger.warn('[fakeCall] Método não disponível no Evolution Channel'); throw new BadRequestException('Method not available on Evolution Channel'); } -} +} \ No newline at end of file diff --git a/src/api/integrations/channel/evolution/evolution.controller.ts b/src/api/integrations/channel/evolution/evolution.controller.ts index c9f36585..e478e55a 100644 --- a/src/api/integrations/channel/evolution/evolution.controller.ts +++ b/src/api/integrations/channel/evolution/evolution.controller.ts @@ -1,39 +1,68 @@ +import { Logger } from '@config/logger.config'; import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { Logger } from '@config/logger.config'; import { ChannelController, ChannelControllerInterface } from '../channel.controller'; export class EvolutionController extends ChannelController implements ChannelControllerInterface { private readonly logger = new Logger('EvolutionController'); - constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { - super(prismaRepository, waMonitor); - } - + // Flag para indicar se a integração está habilitada integrationEnabled: boolean; - public async receiveWebhook(data: any) { - const numberId = data.numberId; + constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { + super(prismaRepository, waMonitor); + this.logger.debug('EvolutionController -> constructor called'); + // Exemplo de log ao definir flags ou propriedades adicionais + this.integrationEnabled = true; + this.logger.debug(`EvolutionController -> integrationEnabled set to: ${this.integrationEnabled}`); + } + + public async receiveWebhook(data: any) { + this.logger.debug('EvolutionController -> receiveWebhook called'); + this.logger.debug(`EvolutionController -> receiveWebhook -> data: ${JSON.stringify(data)}`); + + // Extraindo número de identificação + const numberId = data.numberId; + this.logger.debug(`EvolutionController -> receiveWebhook -> numberId: ${numberId}`); + + // Validando se o numberId foi informado if (!numberId) { this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found'); return; } - const instance = await this.prismaRepository.instance.findFirst({ - where: { number: numberId }, - }); + try { + // Log antes de buscar a instância + this.logger.debug(`EvolutionController -> Looking for instance with numberId: ${numberId}`); + const instance = await this.prismaRepository.instance.findFirst({ + where: { number: numberId }, + }); - if (!instance) { - this.logger.error('WebhookService -> receiveWebhook -> instance not found'); - return; + // Log do resultado da busca + this.logger.debug(`EvolutionController -> Prisma instance result: ${JSON.stringify(instance)}`); + + // Validando se a instância foi encontrada + if (!instance) { + this.logger.error('WebhookService -> receiveWebhook -> instance not found'); + return; + } + + // Log antes de tentar conectar + this.logger.debug(`EvolutionController -> Connecting to WhatsApp instance: ${instance.name}`); + await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); + this.logger.debug('EvolutionController -> Successfully connected to WhatsApp instance'); + + // Retorno de sucesso + this.logger.debug('EvolutionController -> receiveWebhook -> returning success'); + return { + status: 'success', + }; + } catch (error) { + this.logger.error(`EvolutionController -> receiveWebhook -> Error: ${error.message}`); + this.logger.debug(`EvolutionController -> receiveWebhook -> Stack trace: ${error.stack}`); + throw error; } - - await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); - - return { - status: 'success', - }; } } diff --git a/src/api/integrations/channel/evolution/evolution.router.ts b/src/api/integrations/channel/evolution/evolution.router.ts index 1ab0ec00..5d9b70b1 100644 --- a/src/api/integrations/channel/evolution/evolution.router.ts +++ b/src/api/integrations/channel/evolution/evolution.router.ts @@ -1,18 +1,64 @@ +import { Logger } from '@config/logger.config'; import { RouterBroker } from '@api/abstract/abstract.router'; import { evolutionController } from '@api/server.module'; import { ConfigService } from '@config/env.config'; -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; export class EvolutionRouter extends RouterBroker { + private readonly logger = new Logger('EvolutionRouter'); + + public readonly router: Router = Router(); + constructor(readonly configService: ConfigService) { super(); - this.router.post(this.routerPath('webhook/evolution', false), async (req, res) => { - const { body } = req; - const response = await evolutionController.receiveWebhook(body); - return res.status(200).json(response); - }); + // Log the initialization of the EvolutionRouter + this.logger.debug('[EvolutionRouter] Initializing router...'); + + this.router.post( + this.routerPath('webhook/evolution', false), + async (req: Request, res: Response) => { + try { + this.logger.info('[EvolutionRouter] POST /webhook/evolution route called'); + + // Log the request body for debugging (cuidado com dados sensíveis) + const { body } = req; + this.logger.debug( + `[EvolutionRouter] Received request body: ${JSON.stringify(this.sanitizeBody(body))}` + ); + + this.logger.debug('[EvolutionRouter] Calling evolutionController.receiveWebhook...'); + const response = await evolutionController.receiveWebhook(body); + + // Log the response from the controller + this.logger.debug( + `[EvolutionRouter] Response from evolutionController: ${JSON.stringify(response)}` + ); + + this.logger.debug('[EvolutionRouter] Returning 200 with response'); + return res.status(200).json(response); + } catch (error: any) { + // Log the error for debugging + this.logger.error(`[EvolutionRouter] Error in POST /webhook/evolution: ${error.message}`); + return res.status(500).json({ + message: 'Internal server error', + error: error.message, + }); + } + } + ); + + this.logger.debug('[EvolutionRouter] Router setup complete'); } - public readonly router: Router = Router(); -} + /** + * Filters sensitive information from the request body for safe logging. + */ + private sanitizeBody(body: any): any { + // Implement filtering logic to exclude sensitive data + const sanitizedBody = { ...body }; + if (sanitizedBody.password) sanitizedBody.password = '[FILTERED]'; + if (sanitizedBody.token) sanitizedBody.token = '[FILTERED]'; + return sanitizedBody; + } +} \ No newline at end of file diff --git a/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts b/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts index 17cdce01..2f9016dd 100644 --- a/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts +++ b/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts @@ -1,3 +1,4 @@ +import { Logger } from '@config/logger.config'; import { InstanceDto } from '@api/dto/instance.dto'; import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service'; @@ -10,6 +11,8 @@ import { BadRequestException } from '@exceptions'; import { isURL } from 'class-validator'; export class ChatwootController { + private readonly logger = new Logger(ChatwootController.name); + constructor( private readonly chatwootService: ChatwootService, private readonly configService: ConfigService, @@ -17,51 +20,85 @@ export class ChatwootController { ) {} public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); + this.logger.debug(`[createChatwoot] Iniciando criação de Chatwoot para a instância: ${JSON.stringify(instance)}`); + this.logger.debug(`[createChatwoot] Dados recebidos: ${JSON.stringify(data)}`); + + const chatwootConfig = this.configService.get('CHATWOOT'); + if (!chatwootConfig.ENABLED) { + this.logger.warn('[createChatwoot] Chatwoot está desabilitado. Lançando exceção...'); + throw new BadRequestException('Chatwoot is disabled'); + } if (data?.enabled) { + this.logger.debug('[createChatwoot] Validação de dados habilitados...'); + if (!isURL(data.url, { require_tld: false })) { + this.logger.error(`[createChatwoot] URL inválida: ${data.url}`); throw new BadRequestException('url is not valid'); } if (!data.accountId) { + this.logger.error('[createChatwoot] accountId não informado'); throw new BadRequestException('accountId is required'); } if (!data.token) { + this.logger.error('[createChatwoot] token não informado'); throw new BadRequestException('token is required'); } if (data.signMsg !== true && data.signMsg !== false) { + this.logger.error('[createChatwoot] signMsg inválido ou não informado'); throw new BadRequestException('signMsg is required'); } - if (data.signMsg === false) data.signDelimiter = null; + + if (data.signMsg === false) { + this.logger.debug('[createChatwoot] signMsg definido como false, removendo signDelimiter'); + data.signDelimiter = null; + } + } else { + this.logger.debug('[createChatwoot] Dados informam que Chatwoot não está habilitado (enabled=false ou undefined).'); } if (!data.nameInbox || data.nameInbox === '') { + this.logger.debug(`[createChatwoot] nameInbox não informado. Usando nome da instância: "${instance.instanceName}"`); data.nameInbox = instance.instanceName; } + this.logger.debug('[createChatwoot] Chamando ChatwootService.create...'); const result = await this.chatwootService.create(instance, data); + this.logger.debug(`[createChatwoot] Retorno de ChatwootService.create: ${JSON.stringify(result)}`); const urlServer = this.configService.get('SERVER').URL; + this.logger.debug(`[createChatwoot] urlServer obtido: ${urlServer}`); const response = { ...result, webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, }; + this.logger.debug(`[createChatwoot] Retornando resposta final: ${JSON.stringify(response)}`); return response; } public async findChatwoot(instance: InstanceDto): Promise { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); + this.logger.debug(`[findChatwoot] Buscando configurações Chatwoot para a instância: ${JSON.stringify(instance)}`); + const chatwootConfig = this.configService.get('CHATWOOT'); + if (!chatwootConfig.ENABLED) { + this.logger.warn('[findChatwoot] Chatwoot está desabilitado. Lançando exceção...'); + throw new BadRequestException('Chatwoot is disabled'); + } + + this.logger.debug('[findChatwoot] Chamando ChatwootService.find...'); const result = await this.chatwootService.find(instance); + this.logger.debug(`[findChatwoot] Resposta de ChatwootService.find: ${JSON.stringify(result)}`); const urlServer = this.configService.get('SERVER').URL; + this.logger.debug(`[findChatwoot] urlServer obtido: ${urlServer}`); if (Object.keys(result || {}).length === 0) { + this.logger.debug('[findChatwoot] Nenhuma configuração encontrada. Retornando default desabilitado.'); return { enabled: false, url: '', @@ -78,15 +115,28 @@ export class ChatwootController { webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, }; + this.logger.debug(`[findChatwoot] Resposta final: ${JSON.stringify(response)}`); return response; } public async receiveWebhook(instance: InstanceDto, data: any) { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); + this.logger.debug(`[receiveWebhook] Recebendo webhook para instância: ${JSON.stringify(instance)}`); + this.logger.debug(`[receiveWebhook] Dados recebidos no webhook: ${JSON.stringify(data)}`); + const chatwootConfig = this.configService.get('CHATWOOT'); + if (!chatwootConfig.ENABLED) { + this.logger.warn('[receiveWebhook] Chatwoot está desabilitado. Lançando exceção...'); + throw new BadRequestException('Chatwoot is disabled'); + } + + this.logger.debug('[receiveWebhook] Iniciando configuração de CacheService para Chatwoot...'); const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine()); const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache); - return chatwootService.receiveWebhook(instance, data); + this.logger.debug('[receiveWebhook] Chamando chatwootService.receiveWebhook...'); + const result = await chatwootService.receiveWebhook(instance, data); + this.logger.debug(`[receiveWebhook] Resposta de receiveWebhook: ${JSON.stringify(result)}`); + + return result; } } diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 503a5cc9..dd62e4dc 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -31,6 +31,8 @@ import Long from 'long'; import mime from 'mime'; import path from 'path'; import { Readable } from 'stream'; +import { chatbotController } from '@api/server.module'; + interface ChatwootMessage { messageId?: number; @@ -50,48 +52,57 @@ export class ChatwootService { private readonly configService: ConfigService, private readonly prismaRepository: PrismaRepository, private readonly cache: CacheService, - ) {} + ) { + this.logger.log('ChatwootService instanciado'); + } private pgClient = postgresClient.getChatwootConnection(); private async getProvider(instance: InstanceDto): Promise { + this.logger.verbose(`[getProvider] Buscando provider no cache e monitor para: ${instance.instanceName}`); const cacheKey = `${instance.instanceName}:getProvider`; if (await this.cache.has(cacheKey)) { + this.logger.debug(`[getProvider] Provider encontrado em cache para: ${instance.instanceName}`); const provider = (await this.cache.get(cacheKey)) as ChatwootModel; - return provider; } + this.logger.verbose(`[getProvider] Provider não encontrado em cache, buscando via waMonitor...`); const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); if (!provider) { - this.logger.warn('provider not found'); + this.logger.warn('[getProvider] provider não encontrado via waMonitor'); return null; } + this.logger.debug(`[getProvider] Provider encontrado, salvando em cache para: ${instance.instanceName}`); this.cache.set(cacheKey, provider); return provider; } private async clientCw(instance: InstanceDto) { + this.logger.verbose(`[clientCw] Iniciando criação do client Chatwoot para: ${instance.instanceName}`); const provider = await this.getProvider(instance); if (!provider) { - this.logger.error('provider not found'); + this.logger.error('[clientCw] Provider não encontrado, retornando null'); return null; } + this.logger.debug('[clientCw] Provider configurado e definido em this.provider'); this.provider = provider; const client = new ChatwootClient({ config: this.getClientCwConfig(), }); + this.logger.log('[clientCw] Novo ChatwootClient instanciado'); return client; } public getClientCwConfig(): ChatwootAPIConfig & { nameInbox: string; mergeBrazilContacts: boolean } { + this.logger.debug('[getClientCwConfig] Retornando configuração de cliente Chatwoot'); return { basePath: this.provider.url, with_credentials: true, @@ -103,14 +114,16 @@ export class ChatwootService { } public getCache() { + this.logger.debug('[getCache] Retornando serviço de cache'); return this.cache; } public async create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose(`[create] Iniciando criação/atualização de Chatwoot instance: ${JSON.stringify(data)}`); await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); if (data.autoCreate) { - this.logger.log('Auto create chatwoot instance'); + this.logger.log('[create] AutoCreate habilitado, iniciando initInstanceChatwoot'); const urlServer = this.configService.get('SERVER').URL; await this.initInstanceChatwoot( @@ -123,41 +136,46 @@ export class ChatwootService { data.logo, ); } + this.logger.log('[create] Dados de Chatwoot instance retornados'); return data; } public async find(instance: InstanceDto): Promise { + this.logger.verbose(`[find] Buscando dados de Chatwoot para a instância: ${instance.instanceName}`); try { return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); } catch (error) { - this.logger.error('chatwoot not found'); + this.logger.error(`[find] Erro ao buscar Chatwoot: ${error}`); return { enabled: null, url: '' }; } } public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose(`[getContact] Buscando contato ID: ${id} para instância: ${instance.instanceName}`); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[getContact] Cliente Chatwoot não encontrado'); return null; } if (!id) { - this.logger.warn('id is required'); + this.logger.warn('[getContact] ID do contato não fornecido'); return null; } + this.logger.debug(`[getContact] Chamando API do Chatwoot para obter o contato ID: ${id}`); const contact = await client.contact.getContactable({ accountId: this.provider.accountId, id, }); if (!contact) { - this.logger.warn('contact not found'); + this.logger.warn('[getContact] Contato não encontrado'); return null; } + this.logger.debug(`[getContact] Contato encontrado: ${JSON.stringify(contact)}`); return contact; } @@ -170,13 +188,15 @@ export class ChatwootService { organization?: string, logo?: string, ) { + this.logger.verbose('[initInstanceChatwoot] Iniciando criação de Inbox no Chatwoot'); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[initInstanceChatwoot] Client não encontrado'); return null; } + this.logger.verbose('[initInstanceChatwoot] Obtendo lista de inboxes...'); const findInbox: any = await client.inboxes.list({ accountId: this.provider.accountId, }); @@ -185,8 +205,9 @@ export class ChatwootService { let inboxId: number; - this.logger.log('Creating chatwoot inbox'); + this.logger.log('[initInstanceChatwoot] Verificando duplicidade de Inbox'); if (!checkDuplicate) { + this.logger.log(`[initInstanceChatwoot] Inbox ${inboxName} não encontrado, criando novo Inbox`); const data = { type: 'api', webhook_url: webhookUrl, @@ -201,30 +222,31 @@ export class ChatwootService { }); if (!inbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[initInstanceChatwoot] Inbox não pôde ser criado'); return null; } inboxId = inbox.id; + this.logger.log(`[initInstanceChatwoot] Inbox criado com sucesso. ID: ${inboxId}`); } else { + this.logger.log(`[initInstanceChatwoot] Inbox ${inboxName} encontrado, obtendo ID existente`); const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); if (!inbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[initInstanceChatwoot] Inbox não encontrado após verificação duplicada'); return null; } inboxId = inbox.id; + this.logger.log(`[initInstanceChatwoot] Inbox ID reutilizado: ${inboxId}`); } - this.logger.log(`Inbox created - inboxId: ${inboxId}`); if (!this.configService.get('CHATWOOT').BOT_CONTACT) { - this.logger.log('Chatwoot bot contact is disabled'); - + this.logger.log('[initInstanceChatwoot] CHATWOOT.BOT_CONTACT desabilitado, encerrando aqui'); return true; } - this.logger.log('Creating chatwoot bot contact'); + this.logger.log('[initInstanceChatwoot] Criando contato Bot (123456) no Chatwoot'); const contact = (await this.findContact(instance, '123456')) || ((await this.createContact( @@ -237,15 +259,15 @@ export class ChatwootService { )) as any); if (!contact) { - this.logger.warn('contact not found'); + this.logger.warn('[initInstanceChatwoot] Contato bot não foi criado/encontrado'); return null; } const contactId = contact.id || contact.payload.contact.id; - this.logger.log(`Contact created - contactId: ${contactId}`); + this.logger.log(`[initInstanceChatwoot] Contato bot criado/encontrado. ID do contato: ${contactId}`); if (qrcode) { - this.logger.log('QR code enabled'); + this.logger.log('[initInstanceChatwoot] Qrcode habilitado, criando conversa de init...'); const data = { contact_id: contactId.toString(), inbox_id: inboxId.toString(), @@ -257,7 +279,7 @@ export class ChatwootService { }); if (!conversation) { - this.logger.warn('conversation not found'); + this.logger.warn('[initInstanceChatwoot] Conversa não criada/falhou'); return null; } @@ -277,10 +299,10 @@ export class ChatwootService { }); if (!message) { - this.logger.warn('conversation not found'); + this.logger.warn('[initInstanceChatwoot] Mensagem de init não foi enviada'); return null; } - this.logger.log('Init message sent'); + this.logger.log('[initInstanceChatwoot] Mensagem de init enviada com sucesso'); } return true; @@ -295,86 +317,126 @@ export class ChatwootService { avatar_url?: string, jid?: string, ) { + this.logger.verbose(`createContact() -> Criando contato no Chatwoot: phoneNumber=${phoneNumber}, isGroup=${isGroup}, name=${name}`); const client = await this.clientCw(instance); - if (!client) { - this.logger.warn('client not found'); + this.logger.warn(`createContact() -> Client Chatwoot não encontrado para: ${instance.instanceName}`); return null; } let data: any = {}; - if (!isGroup) { - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: jid, - avatar_url: avatar_url, - }; - if ((jid && jid.includes('@')) || !jid) { - data['phone_number'] = `+${phoneNumber}`; - } - } else { + // 1) Se for grupo + if (isGroup) { data = { inbox_id: inboxId, name: name || phoneNumber, identifier: phoneNumber, - avatar_url: avatar_url, + avatar_url, + }; + } + // 2) Se vier webwidget:XYZ (por exemplo "webwidget:163") + else if (jid && jid.startsWith('webwidget:')) { + // Extrair só o número final. Ex.: "163" + const websiteId = jid.split(':')[1] || '0'; + + data = { + inbox_id: inboxId, + identifier: websiteId, // <--- somente "163" + name: name || 'WebsiteUser', + avatar_url, + phone_number: '', + }; + } + // 3) Se o "jid" não tem nenhum "@", interpretamos como ID do website normal (tipo "183") + else if (jid && !jid.includes('@')) { + data = { + inbox_id: inboxId, + identifier: jid, + name: name || 'WebsiteUser', + avatar_url, + phone_number: '', + }; + } + // 4) Se for WhatsApp normal + else { + data = { + inbox_id: inboxId, + identifier: jid, + name: name || phoneNumber, + avatar_url, + phone_number: phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`, }; } + this.logger.debug(`[createContact] Enviando request de data: ${JSON.stringify(data)}`); const contact = await client.contacts.create({ accountId: this.provider.accountId, data, }); if (!contact) { - this.logger.warn('contact not found'); + this.logger.warn('[createContact] Erro ao criar contato'); return null; } + this.logger.debug('[createContact] Contato criado com sucesso, procurando contato para adicionar label...'); const findContact = await this.findContact(instance, phoneNumber); const contactId = findContact?.id; + if (contactId) { + this.logger.log(`[createContact] Adicionando label ao contato ID: ${contactId}`); + await this.addLabelToContact(this.provider.nameInbox, contactId); + } else { + this.logger.warn('[createContact] Contato não encontrado para adicionar label.'); + } - await this.addLabelToContact(this.provider.nameInbox, contactId); - + this.logger.log('[createContact] Contato criado e label atribuída (caso encontrado).'); return contact; } public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose(`[updateContact] Atualizando contato ID: ${id}`); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[updateContact] Client não encontrado'); return null; } if (!id) { - this.logger.warn('id is required'); + this.logger.warn('[updateContact] ID do contato não fornecido'); return null; } try { + this.logger.debug(`[updateContact] Enviando request de update para Chatwoot contato ID: ${id}`); const contact = await client.contacts.update({ accountId: this.provider.accountId, id, data, }); + this.logger.debug('[updateContact] Contato atualizado com sucesso.'); return contact; } catch (error) { + this.logger.error(`[updateContact] Erro ao atualizar contato: ${error}`); return null; } } public async addLabelToContact(nameInbox: string, contactId: number) { + this.logger.verbose(`[addLabelToContact] Iniciando adição de label '${nameInbox}' ao contato ID: ${contactId} via Postgres Chatwoot`,); try { const uri = this.configService.get('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI; - if (!uri) return false; + if (!uri) { + this.logger.warn('[addLabelToContact] URI do banco não configurada. Abortando.'); + return false; + } const sqlTags = `SELECT id, taggings_count FROM tags WHERE name = $1 LIMIT 1`; + this.logger.debug(`[addLabelToContact] Executando query: ${sqlTags}`); const tagData = (await this.pgClient.query(sqlTags, [nameInbox]))?.rows[0]; let tagId = tagData?.id; const taggingsCount = tagData?.taggings_count || 0; @@ -385,73 +447,103 @@ export class ChatwootService { DO UPDATE SET taggings_count = tags.taggings_count + 1 RETURNING id`; + this.logger.debug(`[addLabelToContact] Inserindo/atualizando tags: ${sqlTag}`); tagId = (await this.pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id; const sqlCheckTagging = `SELECT 1 FROM taggings WHERE tag_id = $1 AND taggable_type = 'Contact' AND taggable_id = $2 AND context = 'labels' LIMIT 1`; + this.logger.debug(`[addLabelToContact] Verificando se tagging já existe: ${sqlCheckTagging}`); const taggingExists = (await this.pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0; if (!taggingExists) { const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at) VALUES ($1, 'Contact', $2, 'labels', NOW())`; + this.logger.debug(`[addLabelToContact] Inserindo nova label no tagging: ${sqlInsertLabel}`); await this.pgClient.query(sqlInsertLabel, [tagId, contactId]); + this.logger.verbose('[addLabelToContact] Label adicionada com sucesso ao contato.'); + } else { + this.logger.debug('[addLabelToContact] Label já existente para este contato, não foi necessário inserir.'); } return true; } catch (error) { + this.logger.error(`[addLabelToContact] Erro geral: ${error}`); return false; } } public async findContact(instance: InstanceDto, phoneNumber: string) { - const client = await this.clientCw(instance); + this.logger.debug(`[findContact] Iniciando busca de contato para instance: ${JSON.stringify(instance)}`); + this.logger.debug(`[findContact] phoneNumber recebido: ${phoneNumber}`); - if (!client) { - this.logger.warn('client not found'); + try { + const client = await this.clientCw(instance); + + this.logger.debug('[findContact] Verificando se existe client do Chatwoot...'); + if (!client) { + this.logger.warn('[findContact] client not found'); + return null; + } + + let query: any; + const isGroup = phoneNumber.includes('@g.us'); + this.logger.debug(`[findContact] isGroup: ${isGroup}`); + + if (!isGroup) { + query = `+${phoneNumber}`; + } else { + query = phoneNumber; + } + this.logger.debug(`[findContact] query gerada: ${query}`); + + let contact: any; + this.logger.debug('[findContact] Iniciando pesquisa de contato...'); + + if (isGroup) { + this.logger.debug('[findContact] Buscando contato de grupo via client.contacts.search...'); + contact = await client.contacts.search({ + accountId: this.provider.accountId, + q: query, + }); + } else { + this.logger.debug('[findContact] Buscando contato via /contacts/filter na API do Chatwoot...'); + contact = await chatwootRequest(this.getClientCwConfig(), { + method: 'POST', + url: `/api/v1/accounts/${this.provider.accountId}/contacts/filter`, + body: { + payload: this.getFilterPayload(query), + }, + }); + } + + this.logger.debug(`[findContact] Resultado da busca: ${JSON.stringify(contact)}`); + + // Aqui, vale a pena notar que a verificação original é if (!contact && contact?.payload?.length === 0) + // mas isso pode levar a comportamento inesperado se `contact` for undefined. Sugiro ajustar a checagem: + if (!contact || !contact.payload || contact.payload.length === 0) { + this.logger.warn('[findContact] contact not found'); + return null; + } + + if (!isGroup) { + this.logger.debug('[findContact] Contato não é de grupo. Verificando lista de contatos retornados...'); + return contact.payload.length > 1 + ? this.findContactInContactList(contact.payload, query) + : contact.payload[0]; + } else { + this.logger.debug('[findContact] Contato é de grupo. Verificando se algum item corresponde à query...'); + return contact.payload.find((c: any) => c.identifier === query); + } + } catch (error) { + this.logger.error(`[findContact] Erro ao buscar contato: ${error}`); return null; } - - let query: any; - const isGroup = phoneNumber.includes('@g.us'); - - if (!isGroup) { - query = `+${phoneNumber}`; - } else { - query = phoneNumber; - } - - let contact: any; - - if (isGroup) { - contact = await client.contacts.search({ - accountId: this.provider.accountId, - q: query, - }); - } else { - contact = await chatwootRequest(this.getClientCwConfig(), { - method: 'POST', - url: `/api/v1/accounts/${this.provider.accountId}/contacts/filter`, - body: { - payload: this.getFilterPayload(query), - }, - }); - } - - if (!contact && contact?.payload?.length === 0) { - this.logger.warn('contact not found'); - return null; - } - - if (!isGroup) { - return contact.payload.length > 1 ? this.findContactInContactList(contact.payload, query) : contact.payload[0]; - } else { - return contact.payload.find((contact) => contact.identifier === query); - } } private async mergeBrazilianContacts(contacts: any[]) { + this.logger.verbose('[mergeBrazilianContacts] Tentando unificar contatos com e sem 9 (Brasil)'); try { const contact = await chatwootRequest(this.getClientCwConfig(), { method: 'POST', @@ -462,19 +554,22 @@ export class ChatwootService { }, }); + this.logger.debug('[mergeBrazilianContacts] Merge realizado com sucesso'); return contact; - } catch { - this.logger.error('Error merging contacts'); + } catch (err) { + this.logger.error(`[mergeBrazilianContacts] Erro ao unificar contatos: ${err}`); return null; } } private findContactInContactList(contacts: any[], query: string) { + this.logger.debug(`[findContactInContactList] Verificando lista de contatos duplicados para query: ${query}`); const phoneNumbers = this.getNumbers(query); const searchableFields = this.getSearchableFields(); // eslint-disable-next-line prettier/prettier if (contacts.length === 2 && this.getClientCwConfig().mergeBrazilContacts && query.startsWith('+55')) { + this.logger.debug('[findContactInContactList] Aplicando mergeBrazilianContacts'); const contact = this.mergeBrazilianContacts(contacts); if (contact) { return contact; @@ -488,6 +583,7 @@ export class ChatwootService { const contact_with9 = contacts.find((contact) => contact.phone_number === phone); if (contact_with9) { + this.logger.debug('[findContactInContactList] Contato com 9 encontrado.'); return contact_with9; } @@ -499,10 +595,12 @@ export class ChatwootService { } } + this.logger.warn('[findContactInContactList] Nenhum contato retornado após análise'); return null; } private getNumbers(query: string) { + this.logger.debug(`[getNumbers] Convertendo e padronizando numero: ${query}`); const numbers = []; numbers.push(query); @@ -518,10 +616,12 @@ export class ChatwootService { } private getSearchableFields() { + this.logger.debug('[getSearchableFields] Campos pesquisáveis no Chatwoot: phone_number'); return ['phone_number']; } private getFilterPayload(query: string) { + this.logger.debug(`[getFilterPayload] Montando payload de filtro para query: ${query}`); const filterPayload = []; const numbers = this.getNumbers(query); @@ -542,249 +642,309 @@ export class ChatwootService { return filterPayload; } - public async createConversation(instance: InstanceDto, body: any) { - try { - this.logger.verbose('--- Start createConversation ---'); - this.logger.verbose(`Instance: ${JSON.stringify(instance)}`); +public async createConversation(instance: InstanceDto, body: any) { + try { + this.logger.debug('[createConversation] --- Start createConversation ---'); + this.logger.debug(`[createConversation] Instance recebido: ${JSON.stringify(instance)}`); + this.logger.debug(`[createConversation] Body recebido: ${JSON.stringify(body)}`); - const client = await this.clientCw(instance); + const client = await this.clientCw(instance); - if (!client) { - this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`); - return null; + this.logger.debug('[createConversation] Verificando client do Chatwoot...'); + if (!client) { + this.logger.warn(`[createConversation] Client não encontrado para a instância: ${JSON.stringify(instance)}`,); + return null; + } + + const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`; + this.logger.debug(`[createConversation] cacheKey gerado: ${cacheKey}`); + this.logger.verbose(`Cache key: ${cacheKey}`); + + this.logger.debug('[createConversation] Verificando se existe conversação em cache...'); + if (await this.cache.has(cacheKey)) { + this.logger.debug(`[createConversation] Cache encontrado para key: ${cacheKey}`); + this.logger.verbose(`Cache hit for key: ${cacheKey}`); + const conversationId = (await this.cache.get(cacheKey)) as number; + this.logger.debug(`[createConversation] conversationId em cache: ${conversationId}`); + let conversationExists: conversation | boolean; + this.logger.debug('[createConversation] Tentando buscar conversa existente no Chatwoot...'); + try { + conversationExists = await client.conversations.get({ + accountId: this.provider.accountId, + conversationId: conversationId, + }); + this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`,); + } catch (error) { + this.logger.error(`Error getting conversation: ${error}`); + conversationExists = false; } - const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`; - this.logger.verbose(`Cache key: ${cacheKey}`); + if (!conversationExists) { + this.logger.debug('[createConversation] Conversa não existe mais, limpando cache e recriando...',); + this.logger.verbose('Conversation does not exist, re-calling createConversation'); + this.cache.delete(cacheKey); + return await this.createConversation(instance, body); + } - if (await this.cache.has(cacheKey)) { - this.logger.verbose(`Cache hit for key: ${cacheKey}`); - const conversationId = (await this.cache.get(cacheKey)) as number; - this.logger.verbose(`Cached conversation ID: ${conversationId}`); - let conversationExists: conversation | boolean; - try { - conversationExists = await client.conversations.get({ - accountId: this.provider.accountId, - conversationId: conversationId, + this.logger.debug('[createConversation] Conversa obtida do cache retornada com sucesso'); + return conversationId; + } + + if (body.key.remoteJid && body.key.remoteJid.startsWith('webwidget:')) { + const conversation_id = body.key.remoteJid.split(':')[1] || '0'; + return parseInt(conversation_id); + } + this.logger.debug('[createConversation] Nenhuma conversa encontrada em cache, seguindo fluxo...'); + const isGroup = body.key.remoteJid.includes('@g.us'); + this.logger.debug(`[createConversation] isGroup: ${isGroup}`); + this.logger.verbose(`Is group: ${isGroup}`); + + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; + this.logger.debug(`[createConversation] chatId: ${chatId}`); + this.logger.verbose(`Chat ID: ${chatId}`); + + let nameContact: string; + nameContact = !body.key.fromMe ? body.pushName : chatId; + this.logger.debug(`[createConversation] nameContact: ${nameContact}`); + this.logger.verbose(`Name contact: ${nameContact}`); + + this.logger.debug('[createConversation] Obtendo inbox no Chatwoot...'); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.debug(`[createConversation] Inbox não encontrada para a instância: ${JSON.stringify(instance)}`,); + return null; + } + + if (isGroup) { + this.logger.debug('[createConversation] Conversa de grupo detectada, processando...'); + this.logger.verbose('Processing group conversation'); + const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId,); + this.logger.debug(`[createConversation] groupMetadata: ${JSON.stringify(group)}`); + this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`); + + nameContact = `${group.subject} (GROUP)`; + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + body.key.participant.split('@')[0], + ); + this.logger.debug(`[createConversation] picture_url (participant): ${JSON.stringify(picture_url)}`,); + this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`,); + + const findParticipant = await this.findContact( + instance, + body.key.participant.split('@')[0], + ); + this.logger.debug( `[createConversation] findParticipant: ${JSON.stringify(findParticipant)}`,); + this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`); + + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + this.logger.debug('[createConversation] Atualizando participante no Chatwoot...'); + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, }); - this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`); - } catch (error) { - this.logger.error(`Error getting conversation: ${error}`); - conversationExists = false; } - if (!conversationExists) { - this.logger.verbose('Conversation does not exist, re-calling createConversation'); - this.cache.delete(cacheKey); - return await this.createConversation(instance, body); - } - - return conversationId; - } - - const isGroup = body.key.remoteJid.includes('@g.us'); - this.logger.verbose(`Is group: ${isGroup}`); - - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - this.logger.verbose(`Chat ID: ${chatId}`); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - this.logger.verbose(`Name contact: ${nameContact}`); - - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`); - return null; - } - - if (isGroup) { - this.logger.verbose('Processing group conversation'); - const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); - this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`); - - nameContact = `${group.subject} (GROUP)`; - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + } else { + this.logger.debug('[createConversation] Criando novo contato (participante) no Chatwoot...'); + await this.createContact( + instance, body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + body.key.participant, ); - this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`); + } + } - const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); - this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`); + this.logger.debug('[createConversation] Buscando foto de perfil do contato...'); + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); + this.logger.debug(`[createConversation] picture_url (contato): ${JSON.stringify(picture_url)}`,); + this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`,); - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - body.key.participant, - ); + this.logger.debug('[createConversation] Verificando se contato já existe no Chatwoot...'); + let contact = await this.findContact(instance, chatId); + this.logger.debug(`[createConversation] contact encontrado: ${JSON.stringify(contact)}`); + this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`); + + if (contact) { + if (!body.key.fromMe) { + const waProfilePictureFile = + picture_url?.profilePictureUrl + ?.split('#')[0] + .split('?')[0] + .split('/') + .pop() || ''; + const chatwootProfilePictureFile = + contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || ''; + const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile; + + const nameNeedsUpdate = + !contact.name || + contact.name === chatId || + (`+${chatId}`.startsWith('+55') + ? this.getNumbers(`+${chatId}`).some( + (v) => + contact.name === v || + contact.name === v.substring(3) || + contact.name === v.substring(1), + ) + : false); + + this.logger.debug(`[createConversation] pictureNeedsUpdate: ${pictureNeedsUpdate}`); + this.logger.debug(`[createConversation] nameNeedsUpdate: ${nameNeedsUpdate}`); + this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`); + this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`); + + if (pictureNeedsUpdate || nameNeedsUpdate) { + this.logger.debug('[createConversation] Atualizando contato no Chatwoot...'); + contact = await this.updateContact(instance, contact.id, { + ...(nameNeedsUpdate && { name: nameContact }), + ...(waProfilePictureFile === '' && { avatar: null }), + ...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }), + }); } } + } else { + this.logger.debug('[createConversation] Contato não encontrado. Criando novo contato...'); + const jid = body.key.remoteJid; + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + jid, + ); + } - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); - this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); + if (!contact) { + this.logger.warn('[createConversation] Contato não foi criado ou encontrado.'); + return null; + } - let contact = await this.findContact(instance, chatId); - this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`); + const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + this.logger.debug(`[createConversation] ID do contato: ${contactId}`); + this.logger.verbose(`Contact ID: ${contactId}`); - if (contact) { - if (!body.key.fromMe) { - const waProfilePictureFile = - picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || ''; - const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || ''; - const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile; - const nameNeedsUpdate = - !contact.name || - contact.name === chatId || - (`+${chatId}`.startsWith('+55') - ? this.getNumbers(`+${chatId}`).some( - (v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1), - ) - : false); + this.logger.debug('[createConversation] Listando conversas do contato no Chatwoot...'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.accountId, + id: contactId, + })) as any; + this.logger.debug(`[createConversation] contactConversations: ${JSON.stringify(contactConversations)}`,); + this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`); - this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`); - this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`); + if (!contactConversations || !contactConversations.payload) { + this.logger.error('[createConversation] Nenhuma conversa encontrada ou payload indefinido'); + return null; + } - if (pictureNeedsUpdate || nameNeedsUpdate) { - contact = await this.updateContact(instance, contact.id, { - ...(nameNeedsUpdate && { name: nameContact }), - ...(waProfilePictureFile === '' && { avatar: null }), - ...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }), + if (contactConversations.payload.length) { + let conversation: any; + + if (this.provider.reopenConversation) { + conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); + this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`,); + + if (this.provider.conversationPending) { + if (conversation) { + await client.conversations.toggleStatus({ + accountId: this.provider.accountId, + conversationId: conversation.id, + data: { + status: 'pending', + }, }); } } } else { - const jid = body.key.remoteJid; - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - jid, - ); - } - - if (!contact) { - this.logger.warn('Contact not created or found'); - return null; - } - - const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - this.logger.verbose(`Contact ID: ${contactId}`); - - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.accountId, - id: contactId, - })) as any; - this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`); - - if (!contactConversations || !contactConversations.payload) { - this.logger.error('No conversations found or payload is undefined'); - return null; - } - - if (contactConversations.payload.length) { - let conversation: any; - if (this.provider.reopenConversation) { - conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); - this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`); - - if (this.provider.conversationPending) { - if (conversation) { - await client.conversations.toggleStatus({ - accountId: this.provider.accountId, - conversationId: conversation.id, - data: { - status: 'pending', - }, - }); - } - } - } else { - conversation = contactConversations.payload.find( + this.logger.debug('[createConversation] Verificando conversas não resolvidas...'); + conversation = contactConversations.payload.find( (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`); - } - - if (conversation) { - this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`); - this.cache.set(cacheKey, conversation.id); - return conversation.id; - } + ); + this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`); } - const data = { - contact_id: contactId.toString(), - inbox_id: filterInbox.id.toString(), - }; - - if (this.provider.conversationPending) { - data['status'] = 'pending'; + if (conversation) { + this.logger.debug(`[createConversation] Retornando conversa existente: ID = ${conversation.id}`,); + this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`); + this.cache.set(cacheKey, conversation.id); + return conversation.id; } - - const conversation = await client.conversations.create({ - accountId: this.provider.accountId, - data, - }); - - if (!conversation) { - this.logger.warn('Conversation not created or found'); - return null; - } - - this.logger.verbose(`New conversation created with ID: ${conversation.id}`); - this.cache.set(cacheKey, conversation.id); - return conversation.id; - } catch (error) { - this.logger.error(`Error in createConversation: ${error}`); } + + this.logger.debug('[createConversation] Criando nova conversa no Chatwoot...'); + const data: any = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversationPending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.accountId, + data, + }); + + if (!conversation) { + this.logger.warn('[createConversation] Conversa não foi criada ou não encontrada.'); + return null; + } + + this.logger.debug(`[createConversation] Nova conversa criada com ID: ${conversation.id}`); + this.logger.verbose(`New conversation created with ID: ${conversation.id}`); + this.cache.set(cacheKey, conversation.id); + + this.logger.debug('[createConversation] --- Fim do fluxo de criação de conversa ---'); + return conversation.id; + } catch (error) { + this.logger.error(`[createConversation] Erro em createConversation: ${error}`); + this.logger.error(`Error in createConversation: ${error}`); } +} + public async getInbox(instance: InstanceDto): Promise { + this.logger.verbose('[getInbox] Obtendo inbox pelo nome'); const cacheKey = `${instance.instanceName}:getInbox`; if (await this.cache.has(cacheKey)) { + this.logger.debug('[getInbox] Inbox encontrada em cache'); return (await this.cache.get(cacheKey)) as inbox; } const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[getInbox] Client não encontrado'); return null; } + this.logger.debug(`[getInbox] Procurando inbox pelo nome: ${this.getClientCwConfig().nameInbox}`); const inbox = (await client.inboxes.list({ accountId: this.provider.accountId, })) as any; if (!inbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[getInbox] Nenhum inbox retornado'); return null; } + this.logger.debug(`[getInbox] Procurando inbox pelo nome: ${this.getClientCwConfig().nameInbox}`); const findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().nameInbox); if (!findByName) { - this.logger.warn('inbox not found'); + this.logger.warn('[getInbox] Inbox não encontrado'); return null; } + this.logger.debug('[getInbox] Inbox encontrado e salvo em cache'); this.cache.set(cacheKey, findByName); return findByName; } @@ -804,17 +964,23 @@ export class ChatwootService { sourceId?: string, quotedMsg?: MessageModel, ) { + this.logger.verbose('[createMessage] Criando mensagem no Chatwoot'); + this.logger.debug( + `[createMessage] Parametros => conversationId: ${conversationId}, messageType: ${messageType}, privateMessage: ${privateMessage}, sourceId: ${sourceId}`, + ); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[createMessage] Client não encontrado, retornando null'); return null; } const replyToIds = await this.getReplyToIds(messageBody, instance); + this.logger.debug(`[createMessage] replyToIds: ${JSON.stringify(replyToIds)}`); const sourceReplyId = quotedMsg?.chatwootMessageId || null; + this.logger.debug('[createMessage] Enviando mensagem para ChatwootClient...'); const message = await client.messages.create({ accountId: this.provider.accountId, conversationId: conversationId, @@ -832,10 +998,11 @@ export class ChatwootService { }); if (!message) { - this.logger.warn('message not found'); + this.logger.warn('[createMessage] Falha ao criar mensagem no Chatwoot'); return null; } + this.logger.debug(`[createMessage] Mensagem criada com sucesso: ${JSON.stringify(message)}`); return message; } @@ -844,10 +1011,11 @@ export class ChatwootService { inbox: inbox, contact: generic_id & contact, ): Promise { + this.logger.verbose('[getOpenConversationByContact] Buscando conversa aberta para um contato específico'); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[getOpenConversationByContact] Client não encontrado'); return null; } @@ -856,6 +1024,7 @@ export class ChatwootService { id: contact.id, })) as any; + this.logger.debug(`[getOpenConversationByContact] Verificando conversas do contato ID: ${contact.id}`); return ( conversations.payload.find( (conversation) => conversation.inbox_id === inbox.id && conversation.status === 'open', @@ -873,34 +1042,36 @@ export class ChatwootService { filename: string; }[], ) { + this.logger.verbose(`[createBotMessage] Criando mensagem do bot com o conteúdo: ${content}`); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[createBotMessage] Client não encontrado'); return null; } const contact = await this.findContact(instance, '123456'); if (!contact) { - this.logger.warn('contact not found'); + this.logger.warn('[createBotMessage] Contato Bot (123456) não encontrado'); return null; } const filterInbox = await this.getInbox(instance); if (!filterInbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[createBotMessage] Inbox não encontrado'); return null; } const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact); if (!conversation) { - this.logger.warn('conversation not found'); + this.logger.warn('[createBotMessage] Conversa não encontrada'); return; } + this.logger.debug('[createBotMessage] Enviando mensagem do bot para o Chatwoot...'); const message = await client.messages.create({ accountId: this.provider.accountId, conversationId: conversation.id, @@ -912,10 +1083,11 @@ export class ChatwootService { }); if (!message) { - this.logger.warn('message not found'); + this.logger.warn('[createBotMessage] Falha ao criar mensagem do bot no Chatwoot'); return null; } + this.logger.debug('[createBotMessage] Mensagem do bot criada com sucesso'); return message; } @@ -930,11 +1102,13 @@ export class ChatwootService { sourceId?: string, quotedMsg?: MessageModel, ) { + this.logger.verbose('[sendData] Envio de mídia/arquivo para Chatwoot'); if (sourceId && this.isImportHistoryAvailable()) { + this.logger.debug('[sendData] Verificando se sourceId já está salvo (evitar duplicados) no Chatwoot'); const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]); if (messageAlreadySaved) { if (messageAlreadySaved.size > 0) { - this.logger.warn('Message already saved on chatwoot'); + this.logger.warn('[sendData] Mensagem já salva no Chatwoot, ignorando duplicado'); return null; } } @@ -947,6 +1121,7 @@ export class ChatwootService { data.append('message_type', messageType); + this.logger.debug(`[sendData] Anexando arquivo: ${fileName}`); data.append('attachments[]', fileStream, { filename: fileName }); const sourceReplyId = quotedMsg?.chatwootMessageId || null; @@ -982,11 +1157,13 @@ export class ChatwootService { }; try { + this.logger.debug('[sendData] Fazendo request para Chatwoot com axios'); const { data } = await axios.request(config); + this.logger.debug('[sendData] Mídia/arquivo enviado com sucesso'); return data; } catch (error) { - this.logger.error(error); + this.logger.error(`[sendData] Erro ao enviar arquivo/mídia: ${error}`); } } @@ -997,37 +1174,37 @@ export class ChatwootService { fileStream?: Readable, fileName?: string, ) { + this.logger.verbose('[createBotQr] Criando mensagem de QR Code do bot para Chatwoot'); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[createBotQr] Client não encontrado'); return null; } if (!this.configService.get('CHATWOOT').BOT_CONTACT) { - this.logger.log('Chatwoot bot contact is disabled'); - + this.logger.log('[createBotQr] BOT_CONTACT desabilitado, encerrando'); return true; } const contact = await this.findContact(instance, '123456'); if (!contact) { - this.logger.warn('contact not found'); + this.logger.warn('[createBotQr] Contato Bot (123456) não encontrado'); return null; } const filterInbox = await this.getInbox(instance); if (!filterInbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[createBotQr] Inbox não encontrado'); return null; } const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact); if (!conversation) { - this.logger.warn('conversation not found'); + this.logger.warn('[createBotQr] Conversa não encontrada'); return; } @@ -1055,21 +1232,25 @@ export class ChatwootService { }; try { + this.logger.debug('[createBotQr] Enviando QR code como mensagem do bot ao Chatwoot'); const { data } = await axios.request(config); + this.logger.debug('[createBotQr] QR code enviado com sucesso'); return data; } catch (error) { - this.logger.error(error); + this.logger.error(`[createBotQr] Erro ao enviar QR code: ${error}`); } } public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { + this.logger.verbose(`[sendAttachment] Enviando anexo para WhatsApp: número ${number}, media: ${media}`); try { const parsedMedia = path.parse(decodeURIComponent(media)); let mimeType = mime.getType(parsedMedia?.ext) || ''; let fileName = parsedMedia?.name + parsedMedia?.ext; if (!mimeType) { + this.logger.debug('[sendAttachment] mimeType não identificado diretamente, tentando axios get'); const parts = media.split('/'); fileName = decodeURIComponent(parts[parts.length - 1]); @@ -1097,6 +1278,7 @@ export class ChatwootService { } if (type === 'audio') { + this.logger.debug('[sendAttachment] tipo de arquivo é audio'); const data: SendAudioDto = { number: number, audio: media, @@ -1107,11 +1289,12 @@ export class ChatwootService { sendTelemetry('/message/sendWhatsAppAudio'); const messageSent = await waInstance?.audioWhatsapp(data, true); - + this.logger.verbose('[sendAttachment] Áudio enviado com sucesso'); return messageSent; } if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') { + this.logger.debug('[sendAttachment] Arquivo .gif detectado, enviando como document'); type = 'document'; } @@ -1131,23 +1314,24 @@ export class ChatwootService { } const messageSent = await waInstance?.mediaMessage(data, null, true); - + this.logger.verbose('[sendAttachment] Mídia enviada com sucesso pelo WhatsApp'); return messageSent; } catch (error) { - this.logger.error(error); + this.logger.error(`[sendAttachment] Erro ao enviar anexo: ${error}`); } } public async onSendMessageError(instance: InstanceDto, conversation: number, error?: any) { - this.logger.verbose(`onSendMessageError ${JSON.stringify(error)}`); - + this.logger.verbose(`[onSendMessageError] conversation: ${conversation}, error: ${JSON.stringify(error)}`); const client = await this.clientCw(instance); if (!client) { + this.logger.warn('[onSendMessageError] Client não encontrado'); return; } if (error && error?.status === 400 && error?.message[0]?.exists === false) { + this.logger.debug('[onSendMessageError] Erro indica que número não existe no WhatsApp. Enviando msg privada.'); client.messages.create({ accountId: this.provider.accountId, conversationId: conversation, @@ -1161,6 +1345,7 @@ export class ChatwootService { return; } + this.logger.debug('[onSendMessageError] Enviando msg de erro genérica no Chatwoot.'); client.messages.create({ accountId: this.provider.accountId, conversationId: conversation, @@ -1176,12 +1361,13 @@ export class ChatwootService { public async receiveWebhook(instance: InstanceDto, body: any) { try { + this.logger.verbose(`[receiveWebhook] Recebendo webhook do Chatwoot => Event: ${body.event}`); await new Promise((resolve) => setTimeout(resolve, 500)); const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[receiveWebhook] Client não encontrado'); return null; } @@ -1191,6 +1377,7 @@ export class ChatwootService { body.status === 'resolved' && body.meta?.sender?.identifier ) { + this.logger.debug('[receiveWebhook] conversation_status_changed => resolved, limpando cache...'); const keyToDelete = `${instance.instanceName}:createConversation-${body.meta.sender.identifier}`; this.cache.delete(keyToDelete); } @@ -1200,24 +1387,71 @@ export class ChatwootService { body.private || (body.event === 'message_updated' && !body.content_attributes?.deleted) ) { + this.logger.debug('[receiveWebhook] Evento ignorado (message_updated sem delete OU private)'); return { message: 'bot' }; } + // ---------------------------------------------------- + // 1) SE FOR MENSAGEM CHEGANDO DE WebWidget + // ---------------------------------------------------- + if ( + body.message_type === 'incoming' && + body.conversation.channel === 'Channel::WebWidget' + ) { + this.logger.debug(`(WebWidget) Mensagem incoming do WebWidget: ${body.content}`); + + const evolutionInstance = this.waMonitor.waInstances[instance.instanceName]; + + // Se a instância existir e ainda não tiver localChatwoot.enabled, + // você chama o loadChatwoot() para forçar a leitura do banco: + this.logger.debug(`[receiveWebhook] evolutionInstance.localChatwoot?.enabled: ${evolutionInstance.localChatwoot?.enabled}, evolutionInstance: ${evolutionInstance}`); + if (evolutionInstance && !evolutionInstance.localChatwoot?.enabled) { + this.logger.debug(`[receiveWebhook] Carregando Chatwoot manualmente p/ WebWidget...`); + await evolutionInstance.loadChatwoot(); + } + + const webWidgetMsg = { + key: { + id: body.id, // ID da mensagem do Chatwoot + remoteJid: `webwidget:${body.conversation.id}`, // ou outro "remoteJid" figurativo + fromMe: false, // incoming => do cliente + channel: body.conversation.channel, + inbox_id: body.conversation.inbox_id, + }, + pushName: body.sender?.name || 'WebWidgetUser', + message: { + conversation: body.content, + }, + messageType: 'conversation', + messageTimestamp: Math.floor(Date.now() / 1000), + }; + + await chatbotController.emit({ + instance: { instanceName: instance.instanceName, instanceId: instance.instanceId }, + remoteJid: webWidgetMsg.key.remoteJid, + msg: webWidgetMsg, + pushName: webWidgetMsg.pushName, + }); + + return { message: 'webwidget_incoming_ok' }; + } + const chatId = - body.conversation.meta.sender?.identifier || body.conversation.meta.sender?.phone_number.replace('+', ''); - // Chatwoot to Whatsapp + body.conversation?.meta?.sender?.identifier || + body.conversation?.meta?.sender?.phone_number?.replace('+', ''); const messageReceived = body.content ? body.content - .replaceAll(/(?('CHATWOOT').BOT_CONTACT; if (chatId === '123456' && body.message_type === 'outgoing') { + this.logger.verbose('[receiveWebhook] Mensagem de Comando do Bot Chatwoot detectada'); const command = messageReceived.replace('/', ''); if (cwBotContact && (command.includes('init') || command.includes('iniciar'))) { + this.logger.debug('[receiveWebhook] Comando init: Tentando conectar no WA'); const state = waInstance?.connectionStatus?.state; if (state !== 'open') { @@ -1268,6 +1504,7 @@ export class ChatwootService { } if (command === 'clearcache') { + this.logger.debug('[receiveWebhook] Comando clearcache: Limpando cache'); waInstance.clearCacheChatwoot(); await this.createBotMessage( instance, @@ -1279,6 +1516,7 @@ export class ChatwootService { } if (command === 'status') { + this.logger.debug('[receiveWebhook] Comando status: Verificando estado da instância'); const state = waInstance?.connectionStatus?.state; if (!state) { @@ -1289,9 +1527,7 @@ export class ChatwootService { }), 'incoming', ); - } - - if (state) { + } else { await this.createBotMessage( instance, i18next.t('cw.inbox.status', { @@ -1304,6 +1540,7 @@ export class ChatwootService { } if (cwBotContact && (command === 'disconnect' || command === 'desconectar')) { + this.logger.debug('[receiveWebhook] Comando disconnect: Desconectando instância...'); const msgLogout = i18next.t('cw.inbox.disconnect', { inboxName: body.inbox.name, }); @@ -1316,11 +1553,14 @@ export class ChatwootService { } if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { + this.logger.verbose('[receiveWebhook] Mensagem do Chatwoot -> WhatsApp detectada'); if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') { + this.logger.debug('[receiveWebhook] Mensagem ignorada pois já veio do WhatsApp'); return { message: 'bot' }; } if (!waInstance && body.conversation?.id) { + this.logger.warn('[receiveWebhook] waInstance não encontrado, enviando erro pra Chatwoot'); this.onSendMessageError(instance, body.conversation?.id, 'Instance not found'); return { message: 'bot' }; } @@ -1341,6 +1581,7 @@ export class ChatwootService { for (const message of body.conversation.messages) { if (message.attachments && message.attachments.length > 0) { for (const attachment of message.attachments) { + this.logger.debug('[receiveWebhook] Mensagem com anexo detectado'); if (!messageReceived) { formatText = null; } @@ -1357,6 +1598,7 @@ export class ChatwootService { options, ); if (!messageSent && body.conversation?.id) { + this.logger.warn('[receiveWebhook] Falha ao enviar anexo, chamando onSendMessageError'); this.onSendMessageError(instance, body.conversation?.id); } @@ -1375,6 +1617,7 @@ export class ChatwootService { ); } } else { + this.logger.debug('[receiveWebhook] Mensagem de texto pura, enviando...'); const data: SendTextDto = { number: chatId, text: formatText, @@ -1409,6 +1652,7 @@ export class ChatwootService { instance, ); } catch (error) { + this.logger.error(`[receiveWebhook] Erro ao enviar mensagem de texto: ${error}`); if (!messageSent && body.conversation?.id) { this.onSendMessageError(instance, body.conversation?.id, error); } @@ -1419,6 +1663,7 @@ export class ChatwootService { const chatwootRead = this.configService.get('CHATWOOT').MESSAGE_READ; if (chatwootRead) { + this.logger.debug('[receiveWebhook] chatwootRead habilitado, marcando mensagem como lida'); const lastMessage = await this.prismaRepository.message.findFirst({ where: { key: { @@ -1468,7 +1713,10 @@ export class ChatwootService { } if (body.message_type === 'template' && body.event === 'message_created') { - const data: SendTextDto = { + this.logger.verbose('[receiveWebhook] Mensagem de template criada, enviando texto como broadcast no WA'); + // debugar body + this.logger.debug(`[receiveWebhook] body data2: ${JSON.stringify(body)}`); + const data2: SendTextDto = { number: chatId, text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), delay: 1200, @@ -1476,13 +1724,13 @@ export class ChatwootService { sendTelemetry('/message/sendText'); - await waInstance?.textMessage(data); + await waInstance?.textMessage(data2); + const result = await this.chatwootService.receiveWebhook(instance, dataToSend); } return { message: 'bot' }; } catch (error) { - this.logger.error(error); - + this.logger.error(`[receiveWebhook] Erro geral: ${error}`); return { message: 'bot' }; } } @@ -1492,6 +1740,7 @@ export class ChatwootService { chatwootMessageIds: ChatwootMessage, instance: InstanceDto, ) { + this.logger.verbose('[updateChatwootMessageId] Atualizando ID da mensagem do Chatwoot no banco local'); const key = message.key as { id: string; fromMe: boolean; @@ -1500,9 +1749,11 @@ export class ChatwootService { }; if (!chatwootMessageIds.messageId || !key?.id) { + this.logger.debug('[updateChatwootMessageId] messageId ou key.id não definido'); return; } + this.logger.debug(`[updateChatwootMessageId] Atualizando DB com chatwootMessageIds: ${JSON.stringify(chatwootMessageIds)}`); await this.prismaRepository.message.updateMany({ where: { key: { @@ -1521,11 +1772,13 @@ export class ChatwootService { }); if (this.isImportHistoryAvailable()) { + this.logger.debug('[updateChatwootMessageId] ImportHistory habilitado, atualizando sourceId no import'); chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id); } } private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { + this.logger.debug(`[getMessageByKeyId] Buscando mensagem por keyId: ${keyId}`); const messages = await this.prismaRepository.message.findFirst({ where: { key: { @@ -1543,6 +1796,7 @@ export class ChatwootService { msg: any, instance: InstanceDto, ): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> { + this.logger.debug('[getReplyToIds] Obtendo in_reply_to e in_reply_to_external_id...'); let inReplyTo = null; let inReplyToExternalId = null; @@ -1563,6 +1817,7 @@ export class ChatwootService { } private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { + this.logger.debug('[getQuotedMessage] Verificando mensagem que está sendo respondida...'); if (msg?.content_attributes?.in_reply_to) { const message = await this.prismaRepository.message.findFirst({ where: { @@ -1579,6 +1834,7 @@ export class ChatwootService { }; if (message && key?.id) { + this.logger.debug('[getQuotedMessage] Mensagem de citação encontrada.'); return { key: message.key as proto.IMessageKey, message: message.message as proto.IMessage, @@ -1586,6 +1842,7 @@ export class ChatwootService { } } + this.logger.debug('[getQuotedMessage] Nenhuma mensagem de citação encontrada.'); return null; } @@ -1601,7 +1858,6 @@ export class ChatwootService { ]; const messageKeys = Object.keys(message); - const result = messageKeys.some((key) => media.includes(key)); return result; @@ -1674,7 +1930,7 @@ export class ChatwootService { let result = typeKey ? types[typeKey] : undefined; - // Remove externalAdReplyBody| in Chatwoot (Already Have) + // Remove externalAdReplyBody| em Chatwoot (já é exibido de outra forma) if (result && typeof result === 'string' && result.includes('externalAdReplyBody|')) { result = result.split('externalAdReplyBody|').filter(Boolean).join(''); } @@ -1746,13 +2002,13 @@ export class ChatwootService { )}:_ ${contact.displayName}`; let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; + Object.keys(contactInfo).forEach((k) => { + if (k.startsWith('item') && k.includes('TEL')) { + const phoneNumber = contactInfo[k]; formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`; numberCount++; - } else if (key.includes('TEL')) { - const phoneNumber = contactInfo[key]; + } else if (k.includes('TEL')) { + const phoneNumber = contactInfo[k]; formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`; numberCount++; } @@ -1826,6 +2082,7 @@ export class ChatwootService { } public getConversationMessage(msg: any) { + this.logger.debug('[getConversationMessage] Extraindo texto principal da mensagem do WhatsApp...'); const types = this.getTypeMessage(msg); const messageContent = this.getMessageContent(types); @@ -1835,22 +2092,26 @@ export class ChatwootService { public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { try { + this.logger.log(`[eventWhatsapp] Evento WhatsApp recebido: ${event}`); const waInstance = this.waMonitor.waInstances[instance.instanceName]; if (!waInstance) { - this.logger.warn('wa instance not found'); + this.logger.warn('[eventWhatsapp] Instância WA não encontrada'); return null; } const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[eventWhatsapp] Client Chatwoot não encontrado'); return null; } if (this.provider?.ignoreJids && this.provider?.ignoreJids.length > 0) { - const ignoreJids: any = this.provider?.ignoreJids; + this.logger.debug('[eventWhatsapp] Verificando ignoreJids...'); + const ignoreJids = Array.isArray(this.provider?.ignoreJids) + ? this.provider.ignoreJids + : []; let ignoreGroups = false; let ignoreContacts = false; @@ -1864,33 +2125,35 @@ export class ChatwootService { } if (ignoreGroups && body?.key?.remoteJid.endsWith('@g.us')) { - this.logger.warn('Ignoring message from group: ' + body?.key?.remoteJid); + this.logger.warn(`[eventWhatsapp] Ignorando mensagem de grupo: ${body?.key?.remoteJid}`); return; } if (ignoreContacts && body?.key?.remoteJid.endsWith('@s.whatsapp.net')) { - this.logger.warn('Ignoring message from contact: ' + body?.key?.remoteJid); + this.logger.warn(`[eventWhatsapp] Ignorando mensagem de contato: ${body?.key?.remoteJid}`); return; } if (ignoreJids.includes(body?.key?.remoteJid)) { - this.logger.warn('Ignoring message from jid: ' + body?.key?.remoteJid); + this.logger.warn(`[eventWhatsapp] Ignorando mensagem do JID: ${body?.key?.remoteJid}`); return; } } if (event === 'messages.upsert' || event === 'send.message') { if (body.key.remoteJid === 'status@broadcast') { + this.logger.debug('[eventWhatsapp] Mensagem de status@broadcast, ignorando'); return; } if (body.message?.ephemeralMessage?.message) { + this.logger.debug('[eventWhatsapp] Mensagem ephemeral detectada, convertendo para message normal'); body.message = { ...body.message?.ephemeralMessage?.message, }; } - const originalMessage = await this.getConversationMessage(body.message); + const originalMessage = this.getConversationMessage(body.message); const bodyMessage = originalMessage ? originalMessage .replaceAll(/\*((?!\s)([^\n*]+?)(? {}; // _read is required but you can noop it + fileStream._read = () => {}; fileStream.push(processedBuffer); fileStream.push(null); const truncStr = (str: string, len: number) => { if (!str) return ''; - return str.length > len ? str.substring(0, len) + '...' : str; }; @@ -2090,7 +2356,7 @@ export class ChatwootService { ); if (!send) { - this.logger.warn('message not sent'); + this.logger.warn('[eventWhatsapp] Falha ao enviar ADS message'); return; } @@ -2098,6 +2364,7 @@ export class ChatwootService { } if (body.key.remoteJid.includes('@g.us')) { + this.logger.debug('[eventWhatsapp] Mensagem de grupo sem mídia'); const participantName = body.pushName; let content: string; @@ -2121,12 +2388,13 @@ export class ChatwootService { ); if (!send) { - this.logger.warn('message not sent'); + this.logger.warn('[eventWhatsapp] Falha ao enviar mensagem de grupo'); return; } return send; } else { + this.logger.debug('[eventWhatsapp] Mensagem 1:1 sem mídia'); const send = await this.createMessage( instance, getConversation, @@ -2140,7 +2408,7 @@ export class ChatwootService { ); if (!send) { - this.logger.warn('message not sent'); + this.logger.warn('[eventWhatsapp] Falha ao enviar mensagem 1:1'); return; } @@ -2149,17 +2417,19 @@ export class ChatwootService { } if (event === Events.MESSAGES_DELETE) { + this.logger.debug('[eventWhatsapp] Evento de deleção detectado'); const chatwootDelete = this.configService.get('CHATWOOT').MESSAGE_DELETE; if (chatwootDelete === true) { if (!body?.key?.id) { - this.logger.warn('message id not found'); + this.logger.warn('[eventWhatsapp] key.id não encontrado para deleção, ignorando'); return; } const message = await this.getMessageByKeyId(instance, body.key.id); if (message?.chatwootMessageId && message?.chatwootConversationId) { + this.logger.verbose('[eventWhatsapp] Deletando registro no Prisma e Chatwoot'); await this.prismaRepository.message.deleteMany({ where: { key: { @@ -2180,6 +2450,7 @@ export class ChatwootService { } if (event === 'messages.edit') { + this.logger.verbose('[eventWhatsapp] Mensagem editada detectada'); const editedText = `${ body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text }\n\n_\`${i18next.t('cw.message.edited')}.\`_`; @@ -2208,7 +2479,7 @@ export class ChatwootService { null, ); if (!send) { - this.logger.warn('edited message not sent'); + this.logger.warn('[eventWhatsapp] Falha ao enviar mensagem editada para Chatwoot'); return; } } @@ -2216,8 +2487,9 @@ export class ChatwootService { } if (event === 'messages.read') { + this.logger.verbose('[eventWhatsapp] Evento de mensagem lida detectado'); if (!body?.key?.id || !body?.key?.remoteJid) { - this.logger.warn('message id not found'); + this.logger.warn('[eventWhatsapp] key.id ou key.remoteJid não encontrado, ignorando'); return; } @@ -2232,6 +2504,7 @@ export class ChatwootService { }; if (!sourceId && inbox) { + this.logger.debug('[eventWhatsapp] Buscando dados da conversa no Chatwoot para atualizar last_seen'); const conversation = (await client.conversations.get({ accountId: this.provider.accountId, conversationId: conversationId, @@ -2242,6 +2515,7 @@ export class ChatwootService { } if (sourceId && inbox?.inbox_identifier) { + this.logger.debug('[eventWhatsapp] Atualizando last_seen no Chatwoot...'); const url = `/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` + `/conversations/${conversationId}/update_last_seen`; @@ -2255,11 +2529,12 @@ export class ChatwootService { } if (event === 'status.instance') { + this.logger.debug('[eventWhatsapp] Evento de status da instância detectado'); const data = body; const inbox = await this.getInbox(instance); if (!inbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[eventWhatsapp] Inbox não encontrado ao enviar status de instância'); return; } @@ -2272,9 +2547,10 @@ export class ChatwootService { } if (event === 'connection.update') { + this.logger.debug('[eventWhatsapp] Evento connection.update'); if (body.status === 'open') { - // if we have qrcode count then we understand that a new connection was established if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { + this.logger.debug('[eventWhatsapp] Conexão reestabelecida (qrCode.count > 0)'); const msgConnection = i18next.t('cw.inbox.connected'); await this.createBotMessage(instance, msgConnection, 'incoming'); this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0; @@ -2284,6 +2560,7 @@ export class ChatwootService { } if (event === 'qrcode.updated') { + this.logger.debug('[eventWhatsapp] Evento qrcode.updated'); if (body.statusCode === 500) { const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`; return await this.createBotMessage(instance, erroQRcode, 'incoming'); @@ -2306,28 +2583,29 @@ export class ChatwootService { let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`; if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( - 4, - 8, - )}`; + msgQrCode += `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( + 4, + 8, + )}`; } await this.createBotMessage(instance, msgQrCode, 'incoming'); } } } catch (error) { - this.logger.error(error); + this.logger.error(`[eventWhatsapp] Erro geral: ${error}`); } } public getNumberFromRemoteJid(remoteJid: string) { + this.logger.debug(`[getNumberFromRemoteJid] Extraindo número de: ${remoteJid}`); return remoteJid.replace(/:\d+/, '').split('@')[0]; } public startImportHistoryMessages(instance: InstanceDto) { + this.logger.log('[startImportHistoryMessages] Iniciando processo de import histórico...'); if (!this.isImportHistoryAvailable()) { + this.logger.warn('[startImportHistoryMessages] ImportHistory não está disponível'); return; } @@ -2341,7 +2619,9 @@ export class ChatwootService { } public addHistoryMessages(instance: InstanceDto, messagesRaw: MessageModel[]) { + this.logger.debug('[addHistoryMessages] Adicionando mensagens ao buffer de import histórico...'); if (!this.isImportHistoryAvailable()) { + this.logger.warn('[addHistoryMessages] isImportHistoryAvailable = false, ignorando...'); return; } @@ -2349,7 +2629,9 @@ export class ChatwootService { } public addHistoryContacts(instance: InstanceDto, contactsRaw: ContactModel[]) { + this.logger.debug('[addHistoryContacts] Adicionando contatos ao buffer de import histórico...'); if (!this.isImportHistoryAvailable()) { + this.logger.warn('[addHistoryContacts] isImportHistoryAvailable = false, ignorando...'); return; } @@ -2357,7 +2639,9 @@ export class ChatwootService { } public async importHistoryMessages(instance: InstanceDto) { + this.logger.verbose('[importHistoryMessages] Importando histórico de mensagens para Chatwoot'); if (!this.isImportHistoryAvailable()) { + this.logger.warn('[importHistoryMessages] ImportHistory não disponível, abortando...'); return; } @@ -2382,22 +2666,25 @@ export class ChatwootService { public async updateContactAvatarInRecentConversations(instance: InstanceDto, limitContacts = 100) { try { + this.logger.verbose('[updateContactAvatarInRecentConversations] Atualizando avatars de contatos recentes'); if (!this.isImportHistoryAvailable()) { + this.logger.warn('[updateContactAvatarInRecentConversations] ImportHistory não disponível'); return; } const client = await this.clientCw(instance); if (!client) { - this.logger.warn('client not found'); + this.logger.warn('[updateContactAvatarInRecentConversations] Client não encontrado'); return null; } const inbox = await this.getInbox(instance); if (!inbox) { - this.logger.warn('inbox not found'); + this.logger.warn('[updateContactAvatarInRecentConversations] Inbox não encontrado'); return null; } + this.logger.debug('[updateContactAvatarInRecentConversations] Buscando contatos recentes no Chatwoot...'); const recentContacts = await chatwootImport.getContactsOrderByRecentConversations( inbox, this.provider, @@ -2434,7 +2721,7 @@ export class ChatwootService { } }); } catch (error) { - this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`); + this.logger.error(`[updateContactAvatarInRecentConversations] Erro ao atualizar avatars: ${error.toString()}`); } } @@ -2443,16 +2730,20 @@ export class ChatwootService { chatwootConfig: ChatwootDto, prepareMessage: (message: any) => any, ) { + this.logger.verbose('[syncLostMessages] Sincronizando mensagens perdidas...'); try { if (!this.isImportHistoryAvailable()) { + this.logger.warn('[syncLostMessages] ImportHistory não disponível, abortando...'); return; } if (!this.configService.get('DATABASE').SAVE_DATA.MESSAGE_UPDATE) { + this.logger.warn('[syncLostMessages] MESSAGE_UPDATE desabilitado, abortando...'); return; } const inbox = await this.getInbox(instance); + this.logger.debug('[syncLostMessages] Montando SQL para buscar mensagens no PG do Chatwoot...'); const sqlMessages = `select * from messages m where account_id = ${chatwootConfig.accountId} and inbox_id = ${inbox.id} @@ -2460,6 +2751,7 @@ export class ChatwootService { order by created_at desc`; const messagesData = (await this.pgClient.query(sqlMessages))?.rows; + this.logger.debug(`[syncLostMessages] Mensagens do Chatwoot obtidas, total: ${messagesData.length}`); const ids: string[] = messagesData .filter((message) => !!message.source_id) .map((message) => message.source_id.replace('WAID:', '')); @@ -2472,6 +2764,7 @@ export class ChatwootService { }, }); + this.logger.debug(`[syncLostMessages] Filtrando messages do Prisma. total: ${savedMessages.length}`); const filteredMessages = savedMessages.filter( (msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid), ); @@ -2488,6 +2781,7 @@ export class ChatwootService { messagesRaw.push(prepareMessage(m as any)); } + this.logger.debug(`[syncLostMessages] Total de mensagens para import: ${messagesRaw.length}`); this.addHistoryMessages( instance, messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)), @@ -2496,7 +2790,9 @@ export class ChatwootService { await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider); const waInstance = this.waMonitor.waInstances[instance.instanceName]; waInstance.clearCacheChatwoot(); + this.logger.verbose('[syncLostMessages] Processo de sincronização finalizado com sucesso.'); } catch (error) { + this.logger.error(`[syncLostMessages] Erro durante syncLostMessages: ${error}`); return; } } diff --git a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts index 3d28ff4a..4387c02e 100644 --- a/src/api/integrations/chatbot/openai/controllers/openai.controller.ts +++ b/src/api/integrations/chatbot/openai/controllers/openai.controller.ts @@ -1,3 +1,4 @@ +import { Logger } from '@config/logger.config'; 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'; @@ -5,7 +6,6 @@ import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai. import { PrismaRepository } from '@api/repository/repository.service'; import { WAMonitoringService } from '@api/services/monitor.service'; import { configService, Openai } from '@config/env.config'; -import { Logger } from '@config/logger.config'; import { BadRequestException } from '@exceptions'; import { OpenaiBot } from '@prisma/client'; import { getConversationMessage } from '@utils/getConversationMessage'; @@ -39,20 +39,34 @@ export class OpenaiController extends ChatbotController implements ChatbotContro // Credentials public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if (!data.apiKey) throw new Error('API Key is required'); - if (!data.name) throw new Error('Name is required'); + this.logger.debug(`[createOpenaiCreds] -> Iniciando método com instance: ${JSON.stringify(instance)} e data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[createOpenaiCreds] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[createOpenaiCreds] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + if (!data.apiKey) { + this.logger.error('[createOpenaiCreds] -> Falha: API Key não fornecida.'); + throw new Error('API Key is required'); + } + if (!data.name) { + this.logger.error('[createOpenaiCreds] -> Falha: Nome não fornecido.'); + throw new Error('Name is required'); + } + + this.logger.debug('[createOpenaiCreds] -> Tentando criar credenciais no banco...'); const creds = await this.credsRepository.create({ data: { name: data.name, @@ -61,270 +75,333 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[createOpenaiCreds] -> Credenciais criadas com sucesso: ${JSON.stringify(creds)}`); return creds; } catch (error) { - this.logger.error(error); + this.logger.error(`[createOpenaiCreds] -> Erro ao criar credenciais: ${error}`); throw new Error('Error creating openai creds'); } } public async findOpenaiCreds(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const creds = await this.credsRepository.findMany({ - where: { - instanceId: instanceId, - }, - include: { - OpenaiAssistant: true, - }, - }); - - return creds; - } - - public async deleteCreds(instance: InstanceDto, openaiCredsId: string) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - const creds = await this.credsRepository.findFirst({ - where: { - id: openaiCredsId, - }, - }); - - if (!creds) { - throw new Error('Openai Creds not found'); - } - - if (creds.instanceId !== instanceId) { - throw new Error('Openai Creds not found'); + this.logger.debug(`[findOpenaiCreds] -> Iniciando método com instance: ${JSON.stringify(instance)}`); + if (!this.integrationEnabled) { + this.logger.warn('[findOpenaiCreds] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); } try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[findOpenaiCreds] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const creds = await this.credsRepository.findMany({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiAssistant: true, + }, + }); + + this.logger.debug(`[findOpenaiCreds] -> Credenciais encontradas: ${JSON.stringify(creds)}`); + return creds; + } catch (error) { + this.logger.error(`[findOpenaiCreds] -> Erro ao buscar credenciais: ${error}`); + throw error; + } + } + + public async deleteCreds(instance: InstanceDto, openaiCredsId: string) { + this.logger.debug(`[deleteCreds] -> Iniciando método com instance: ${JSON.stringify(instance)} e openaiCredsId: ${openaiCredsId}`); + if (!this.integrationEnabled) { + this.logger.warn('[deleteCreds] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[deleteCreds] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const creds = await this.credsRepository.findFirst({ + where: { + id: openaiCredsId, + }, + }); + + if (!creds) { + this.logger.warn(`[deleteCreds] -> Credenciais não encontradas com id: ${openaiCredsId}`); + throw new Error('Openai Creds not found'); + } + + if (creds.instanceId !== instanceId) { + this.logger.warn('[deleteCreds] -> Credenciais não pertencem a esta instância'); + throw new Error('Openai Creds not found'); + } + + this.logger.debug('[deleteCreds] -> Tentando deletar credenciais no banco...'); await this.credsRepository.delete({ where: { id: openaiCredsId, }, }); + this.logger.debug(`[deleteCreds] -> Credenciais deletadas com sucesso: ${openaiCredsId}`); return { openaiCreds: { id: openaiCredsId } }; } catch (error) { - this.logger.error(error); + this.logger.error(`[deleteCreds] -> Erro ao deletar credenciais: ${error}`); throw new Error('Error deleting openai creds'); } } // Models public async getModels(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if (!instanceId) throw new Error('Instance not found'); - - const defaultSettings = await this.settingsRepository.findFirst({ - where: { - instanceId: instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); - - if (!defaultSettings) throw new Error('Settings not found'); - - const { apiKey } = defaultSettings.OpenaiCreds; + this.logger.debug(`[getModels] -> Iniciando método com instance: ${JSON.stringify(instance)}`); + if (!this.integrationEnabled) { + this.logger.warn('[getModels] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[getModels] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + if (!instanceId) { + this.logger.warn('[getModels] -> Instância não encontrada.'); + throw new Error('Instance not found'); + } + + const defaultSettings = await this.settingsRepository.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiCreds: true, + }, + }); + + if (!defaultSettings) { + this.logger.warn('[getModels] -> Configurações padrão não encontradas.'); + throw new Error('Settings not found'); + } + + const { apiKey } = defaultSettings.OpenaiCreds; + this.logger.debug(`[getModels] -> Criando cliente OpenAI com apiKey: ${apiKey ? '*****' : 'não fornecida'}`); + this.client = new OpenAI({ apiKey }); + this.logger.debug('[getModels] -> Buscando lista de modelos no OpenAI...'); const models: any = await this.client.models.list(); + this.logger.debug(`[getModels] -> Modelos retornados: ${JSON.stringify(models?.body?.data)}`); return models?.body?.data; } catch (error) { - this.logger.error(error); + this.logger.error(`[getModels] -> Erro ao buscar modelos: ${error}`); throw new Error('Error fetching models'); } } // Bots public async createBot(instance: InstanceDto, data: OpenaiDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); - - const instanceId = await this.prismaRepository.instance - .findFirst({ - where: { - name: instance.instanceName, - }, - }) - .then((instance) => instance.id); - - if ( - !data.openaiCredsId || - !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 (!data.openaiCredsId) { - throw new Error('Openai Creds Id is required'); - } - - if (!defaultSettingCheck) { - await this.settings(instance, { - openaiCredsId: data.openaiCredsId, - 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 openai with an "All" trigger, you cannot have more bots while it is active'); - } - - let whereDuplication: any = { - instanceId: instanceId, - }; - - if (data.botType === 'assistant') { - if (!data.assistantId) throw new Error('Assistant ID is required'); - - whereDuplication = { - ...whereDuplication, - assistantId: data.assistantId, - botType: data.botType, - }; - } else if (data.botType === 'chatCompletion') { - if (!data.model) throw new Error('Model is required'); - if (!data.maxTokens) throw new Error('Max tokens is required'); - - whereDuplication = { - ...whereDuplication, - model: data.model, - maxTokens: data.maxTokens, - botType: data.botType, - }; - } else { - throw new Error('Bot type is required'); - } - - const checkDuplicate = await this.botRepository.findFirst({ - where: whereDuplication, - }); - - if (checkDuplicate) { - throw new Error('Openai 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, - 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'); - } + this.logger.debug(`[createBot] -> Iniciando método com instance: ${JSON.stringify(instance)} e data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[createBot] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); } try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[createBot] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + this.logger.debug('[createBot] -> Verificando e aplicando configurações padrão, caso necessário...'); + + if ( + !data.openaiCredsId || + !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 (!data.openaiCredsId) { + this.logger.error('[createBot] -> Falha: openaiCredsId não foi fornecido e é obrigatório.'); + throw new Error('Openai Creds Id is required'); + } + + if (!defaultSettingCheck) { + this.logger.debug('[createBot] -> Não existem configurações padrão, criando...'); + await this.settings(instance, { + openaiCredsId: data.openaiCredsId, + 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, + }); + } + } + + this.logger.debug('[createBot] -> Verificando se já existe bot com triggerType = "all" habilitado...'); + const checkTriggerAll = await this.botRepository.findFirst({ + where: { + enabled: true, + triggerType: 'all', + instanceId: instanceId, + }, + }); + + if (checkTriggerAll && data.triggerType === 'all') { + this.logger.error('[createBot] -> Já existe um bot com triggerType "all" habilitado, falha.'); + throw new Error('You already have a openai with an "All" trigger, you cannot have more bots while it is active'); + } + + let whereDuplication: any = { + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) { + this.logger.error('[createBot] -> Falha: assistantId não fornecido para botType=assistant.'); + throw new Error('Assistant ID is required'); + } + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + botType: data.botType, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) { + this.logger.error('[createBot] -> Falha: model não fornecido para botType=chatCompletion.'); + throw new Error('Model is required'); + } + if (!data.maxTokens) { + this.logger.error('[createBot] -> Falha: maxTokens não fornecido para botType=chatCompletion.'); + throw new Error('Max tokens is required'); + } + + whereDuplication = { + ...whereDuplication, + model: data.model, + maxTokens: data.maxTokens, + botType: data.botType, + }; + } else { + this.logger.error('[createBot] -> Falha: botType não fornecido.'); + throw new Error('Bot type is required'); + } + + this.logger.debug('[createBot] -> Verificando duplicação de bot...'); + const checkDuplicate = await this.botRepository.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + this.logger.error('[createBot] -> Bot duplicado encontrado, falha.'); + throw new Error('Openai Bot already exists'); + } + + if (data.triggerType === 'keyword') { + if (!data.triggerOperator || !data.triggerValue) { + this.logger.error('[createBot] -> Falha: triggerOperator ou triggerValue não fornecido para triggerType=keyword.'); + 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) { + this.logger.error('[createBot] -> Trigger duplicado encontrado para triggerType=keyword.'); + throw new Error('Trigger already exists'); + } + } + + if (data.triggerType === 'advanced') { + if (!data.triggerValue) { + this.logger.error('[createBot] -> Falha: triggerValue não fornecido para triggerType=advanced.'); + throw new Error('Trigger value is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerValue: data.triggerValue, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + this.logger.error('[createBot] -> Trigger duplicado encontrado para triggerType=advanced.'); + throw new Error('Trigger already exists'); + } + } + + this.logger.debug('[createBot] -> Criando bot no banco de dados...'); const bot = await this.botRepository.create({ data: { enabled: data?.enabled, @@ -356,183 +433,241 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[createBot] -> Bot criado com sucesso: ${JSON.stringify(bot)}`); return bot; } catch (error) { - this.logger.error(error); + this.logger.error(`[createBot] -> Erro ao criar bot: ${error}`); throw new Error('Error creating openai bot'); } } public async findBot(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai 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, - }, - }); - - if (!bots.length) { - return null; - } - - return bots; - } - - public async fetchBot(instance: InstanceDto, botId: string) { - if (!this.integrationEnabled) throw new BadRequestException('Openai 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('Openai Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Openai Bot not found'); - } - - return bot; - } - - public async updateBot(instance: InstanceDto, botId: string, data: OpenaiDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai 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('Openai Bot not found'); - } - - if (bot.instanceId !== instanceId) { - throw new Error('Openai 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 openai bot with an "All" trigger, you cannot have more bots while it is active', - ); - } - } - - let whereDuplication: any = { - id: { - not: botId, - }, - instanceId: instanceId, - }; - - if (data.botType === 'assistant') { - if (!data.assistantId) throw new Error('Assistant ID is required'); - - whereDuplication = { - ...whereDuplication, - assistantId: data.assistantId, - }; - } else if (data.botType === 'chatCompletion') { - if (!data.model) throw new Error('Model is required'); - if (!data.maxTokens) throw new Error('Max tokens is required'); - - whereDuplication = { - ...whereDuplication, - model: data.model, - maxTokens: data.maxTokens, - }; - } else { - throw new Error('Bot type is required'); - } - - const checkDuplicate = await this.botRepository.findFirst({ - where: whereDuplication, - }); - - if (checkDuplicate) { - throw new Error('Openai 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'); - } + this.logger.debug(`[findBot] -> Iniciando método com instance: ${JSON.stringify(instance)}`); + if (!this.integrationEnabled) { + this.logger.warn('[findBot] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); } try { - const bot = await this.botRepository.update({ + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[findBot] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const bots = await this.botRepository.findMany({ + where: { + instanceId, + }, + }); + + this.logger.debug(`[findBot] -> Bots encontrados: ${JSON.stringify(bots)}`); + if (!bots.length) { + this.logger.debug('[findBot] -> Nenhum bot encontrado.'); + return null; + } + + return bots; + } catch (error) { + this.logger.error(`[findBot] -> Erro ao buscar bots: ${error}`); + throw error; + } + } + + public async fetchBot(instance: InstanceDto, botId: string) { + this.logger.debug(`[fetchBot] -> Iniciando método com instance: ${JSON.stringify(instance)} e botId: ${botId}`); + if (!this.integrationEnabled) { + this.logger.warn('[fetchBot] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[fetchBot] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const bot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!bot) { + this.logger.warn(`[fetchBot] -> Bot não encontrado com id: ${botId}`); + throw new Error('Openai Bot not found'); + } + + if (bot.instanceId !== instanceId) { + this.logger.warn('[fetchBot] -> Bot não pertence a esta instância'); + throw new Error('Openai Bot not found'); + } + + this.logger.debug(`[fetchBot] -> Bot encontrado: ${JSON.stringify(bot)}`); + return bot; + } catch (error) { + this.logger.error(`[fetchBot] -> Erro ao buscar bot: ${error}`); + throw error; + } + } + + public async updateBot(instance: InstanceDto, botId: string, data: OpenaiDto) { + this.logger.debug(`[updateBot] -> Iniciando método com instance: ${JSON.stringify(instance)}, botId: ${botId} e data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[updateBot] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } + + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[updateBot] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const bot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!bot) { + this.logger.warn(`[updateBot] -> Bot não encontrado com id: ${botId}`); + throw new Error('Openai Bot not found'); + } + + if (bot.instanceId !== instanceId) { + this.logger.warn('[updateBot] -> Bot não pertence a esta instância'); + throw new Error('Openai Bot not found'); + } + + if (data.triggerType === 'all') { + this.logger.debug('[updateBot] -> Verificando se já existe outro bot com triggerType = "all" habilitado...'); + const checkTriggerAll = await this.botRepository.findFirst({ + where: { + enabled: true, + triggerType: 'all', + id: { + not: botId, + }, + instanceId: instanceId, + }, + }); + + if (checkTriggerAll) { + this.logger.error('[updateBot] -> Já existe um bot com triggerType "all" habilitado, falha.'); + throw new Error( + 'You already have a openai bot with an "All" trigger, you cannot have more bots while it is active', + ); + } + } + + let whereDuplication: any = { + id: { + not: botId, + }, + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) { + this.logger.error('[updateBot] -> Falha: assistantId não fornecido para botType=assistant.'); + throw new Error('Assistant ID is required'); + } + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) { + this.logger.error('[updateBot] -> Falha: model não fornecido para botType=chatCompletion.'); + throw new Error('Model is required'); + } + if (!data.maxTokens) { + this.logger.error('[updateBot] -> Falha: maxTokens não fornecido para botType=chatCompletion.'); + throw new Error('Max tokens is required'); + } + + whereDuplication = { + ...whereDuplication, + model: data.model, + maxTokens: data.maxTokens, + }; + } else { + this.logger.error('[updateBot] -> Falha: botType não fornecido.'); + throw new Error('Bot type is required'); + } + + this.logger.debug('[updateBot] -> Verificando duplicação de bot...'); + const checkDuplicate = await this.botRepository.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + this.logger.error('[updateBot] -> Bot duplicado encontrado, falha.'); + throw new Error('Openai Bot already exists'); + } + + if (data.triggerType === 'keyword') { + if (!data.triggerOperator || !data.triggerValue) { + this.logger.error('[updateBot] -> Falha: triggerOperator ou triggerValue não fornecido para triggerType=keyword.'); + 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) { + this.logger.error('[updateBot] -> Trigger duplicado encontrado para triggerType=keyword.'); + throw new Error('Trigger already exists'); + } + } + + if (data.triggerType === 'advanced') { + if (!data.triggerValue) { + this.logger.error('[updateBot] -> Falha: triggerValue não fornecido para triggerType=advanced.'); + throw new Error('Trigger value is required'); + } + + const checkDuplicate = await this.botRepository.findFirst({ + where: { + triggerValue: data.triggerValue, + id: { not: botId }, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + this.logger.error('[updateBot] -> Trigger duplicado encontrado para triggerType=advanced.'); + throw new Error('Trigger already exists'); + } + } + + this.logger.debug('[updateBot] -> Atualizando bot no banco de dados...'); + const updatedBot = await this.botRepository.update({ where: { id: botId, }, @@ -566,38 +701,50 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); - return bot; + this.logger.debug(`[updateBot] -> Bot atualizado com sucesso: ${JSON.stringify(updatedBot)}`); + return updatedBot; } catch (error) { - this.logger.error(error); + this.logger.error(`[updateBot] -> Erro ao atualizar bot: ${error}`); throw new Error('Error updating openai bot'); } } public async deleteBot(instance: InstanceDto, botId: string) { - if (!this.integrationEnabled) throw new BadRequestException('Openai 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('Openai bot not found'); + this.logger.debug(`[deleteBot] -> Iniciando método com instance: ${JSON.stringify(instance)} e botId: ${botId}`); + if (!this.integrationEnabled) { + this.logger.warn('[deleteBot] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); } - if (bot.instanceId !== instanceId) { - throw new Error('Openai bot not found'); - } try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((inst) => { + this.logger.debug(`[deleteBot] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); + + const bot = await this.botRepository.findFirst({ + where: { + id: botId, + }, + }); + + if (!bot) { + this.logger.warn(`[deleteBot] -> Bot não encontrado com id: ${botId}`); + throw new Error('Openai bot not found'); + } + + if (bot.instanceId !== instanceId) { + this.logger.warn('[deleteBot] -> Bot não pertence a esta instância'); + throw new Error('Openai bot not found'); + } + + this.logger.debug('[deleteBot] -> Deletando sessões e bot no banco...'); await this.sessionRepository.deleteMany({ where: { botId: botId, @@ -610,16 +757,21 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[deleteBot] -> Bot deletado com sucesso: ${botId}`); return { bot: { id: botId } }; } catch (error) { - this.logger.error(error); + this.logger.error(`[deleteBot] -> Erro ao deletar bot: ${error}`); throw new Error('Error deleting openai bot'); } } // Settings public async settings(instance: InstanceDto, data: any) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + this.logger.debug(`[settings] -> Iniciando método com instance: ${JSON.stringify(instance)}, data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[settings] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { const instanceId = await this.prismaRepository.instance @@ -628,7 +780,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro name: instance.instanceName, }, }) - .then((instance) => instance.id); + .then((inst) => { + this.logger.debug(`[settings] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); const settings = await this.settingsRepository.findFirst({ where: { @@ -637,6 +792,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); if (settings) { + this.logger.debug('[settings] -> Atualizando configurações existentes...'); const updateSettings = await this.settingsRepository.update({ where: { id: settings.id, @@ -659,6 +815,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[settings] -> Configurações atualizadas: ${JSON.stringify(updateSettings)}`); return { openaiCredsId: updateSettings.openaiCredsId, expire: updateSettings.expire, @@ -677,6 +834,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }; } + this.logger.debug('[settings] -> Criando novas configurações...'); const newSetttings = await this.settingsRepository.create({ data: { openaiCredsId: data.openaiCredsId, @@ -697,6 +855,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[settings] -> Novas configurações criadas: ${JSON.stringify(newSetttings)}`); return { openaiCredsId: newSetttings.openaiCredsId, expire: newSetttings.expire, @@ -714,13 +873,17 @@ export class OpenaiController extends ChatbotController implements ChatbotContro timePerChar: newSetttings.timePerChar, }; } catch (error) { - this.logger.error(error); + this.logger.error(`[settings] -> Erro ao criar/atualizar configurações: ${error}`); throw new Error('Error setting default settings'); } } public async fetchSettings(instance: InstanceDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + this.logger.debug(`[fetchSettings] -> Iniciando método com instance: ${JSON.stringify(instance)}`); + if (!this.integrationEnabled) { + this.logger.warn('[fetchSettings] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { const instanceId = ( @@ -732,6 +895,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }) )?.id; + this.logger.debug(`[fetchSettings] -> Buscando configurações com instanceId: ${instanceId}`); const settings = await this.settingsRepository.findFirst({ where: { instanceId: instanceId, @@ -742,6 +906,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); if (!settings) { + this.logger.debug('[fetchSettings] -> Nenhuma configuração encontrada, retornando padrão vazio.'); return { openaiCredsId: null, expire: 0, @@ -760,6 +925,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }; } + this.logger.debug(`[fetchSettings] -> Configurações encontradas: ${JSON.stringify(settings)}`); return { openaiCredsId: settings.openaiCredsId, expire: settings.expire, @@ -777,14 +943,18 @@ export class OpenaiController extends ChatbotController implements ChatbotContro fallback: settings.Fallback, }; } catch (error) { - this.logger.error(error); + this.logger.error(`[fetchSettings] -> Erro ao buscar configurações: ${error}`); throw new Error('Error fetching default settings'); } } // Sessions public async changeStatus(instance: InstanceDto, data: any) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + this.logger.debug(`[changeStatus] -> Iniciando método com instance: ${JSON.stringify(instance)} e data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[changeStatus] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { const instanceId = await this.prismaRepository.instance @@ -793,7 +963,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro name: instance.instanceName, }, }) - .then((instance) => instance.id); + .then((inst) => { + this.logger.debug(`[changeStatus] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); const defaultSettingCheck = await this.settingsRepository.findFirst({ where: { @@ -804,7 +977,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro const remoteJid = data.remoteJid; const status = data.status; + this.logger.debug(`[changeStatus] -> remoteJid: ${remoteJid}, status: ${status}`); + if (status === 'delete') { + this.logger.debug('[changeStatus] -> Deletando todas as sessões para este remoteJid...'); await this.sessionRepository.deleteMany({ where: { remoteJid: remoteJid, @@ -812,11 +988,13 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug('[changeStatus] -> Sessões deletadas com sucesso.'); return { openai: { remoteJid: remoteJid, status: status } }; } if (status === 'closed') { if (defaultSettingCheck?.keepOpen) { + this.logger.debug('[changeStatus] -> Configuração keepOpen habilitada, definindo status=closed para as sessões...'); await this.sessionRepository.updateMany({ where: { remoteJid: remoteJid, @@ -828,6 +1006,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); } else { + this.logger.debug('[changeStatus] -> keepOpen desabilitado, deletando sessões...'); await this.sessionRepository.deleteMany({ where: { remoteJid: remoteJid, @@ -835,8 +1014,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); } + this.logger.debug('[changeStatus] -> Sessões tratadas com sucesso.'); return { openai: { ...instance, openai: { remoteJid: remoteJid, status: status } } }; } else { + this.logger.debug('[changeStatus] -> Atualizando status das sessões existentes...'); const session = await this.sessionRepository.updateMany({ where: { instanceId: instanceId, @@ -854,16 +1035,21 @@ export class OpenaiController extends ChatbotController implements ChatbotContro session, }; + this.logger.debug(`[changeStatus] -> Sessão atualizada com sucesso: ${JSON.stringify(openaiData)}`); return { openai: { ...instance, openai: openaiData } }; } } catch (error) { - this.logger.error(error); + this.logger.error(`[changeStatus] -> Erro ao alterar status: ${error}`); throw new Error('Error changing status'); } } public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + this.logger.debug(`[fetchSessions] -> Iniciando método com instance: ${JSON.stringify(instance)}, botId: ${botId}, remoteJid: ${remoteJid}`); + if (!this.integrationEnabled) { + this.logger.warn('[fetchSessions] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { const instanceId = await this.prismaRepository.instance @@ -872,7 +1058,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro name: instance.instanceName, }, }) - .then((instance) => instance.id); + .then((inst) => { + this.logger.debug(`[fetchSessions] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); const openaiBot = await this.botRepository.findFirst({ where: { @@ -881,10 +1070,12 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); if (openaiBot && openaiBot.instanceId !== instanceId) { + this.logger.warn('[fetchSessions] -> Bot não pertence a esta instância.'); throw new Error('Openai Bot not found'); } - return await this.sessionRepository.findMany({ + this.logger.debug('[fetchSessions] -> Buscando sessões no banco...'); + const sessions = await this.sessionRepository.findMany({ where: { instanceId: instanceId, remoteJid, @@ -892,14 +1083,21 @@ export class OpenaiController extends ChatbotController implements ChatbotContro type: 'openai', }, }); + + this.logger.debug(`[fetchSessions] -> Sessões encontradas: ${JSON.stringify(sessions)}`); + return sessions; } catch (error) { - this.logger.error(error); + this.logger.error(`[fetchSessions] -> Erro ao buscar sessões: ${error}`); throw new Error('Error fetching sessions'); } } public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) { - if (!this.integrationEnabled) throw new BadRequestException('Openai is disabled'); + this.logger.debug(`[ignoreJid] -> Iniciando método com instance: ${JSON.stringify(instance)}, data: ${JSON.stringify(data)}`); + if (!this.integrationEnabled) { + this.logger.warn('[ignoreJid] -> OpenAI está desabilitado, lançando exceção.'); + throw new BadRequestException('Openai is disabled'); + } try { const instanceId = await this.prismaRepository.instance @@ -908,7 +1106,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro name: instance.instanceName, }, }) - .then((instance) => instance.id); + .then((inst) => { + this.logger.debug(`[ignoreJid] -> instanceId obtido: ${inst?.id}`); + return inst.id; + }); const settings = await this.settingsRepository.findFirst({ where: { @@ -917,19 +1118,26 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); if (!settings) { + this.logger.warn('[ignoreJid] -> Configurações não encontradas para esta instância.'); throw new Error('Settings not found'); } let ignoreJids: any = settings?.ignoreJids || []; if (data.action === 'add') { - if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids }; + this.logger.debug('[ignoreJid] -> Adicionando remoteJid à lista de ignorados...'); + if (ignoreJids.includes(data.remoteJid)) { + this.logger.debug('[ignoreJid] -> Jid já está na lista de ignorados.'); + return { ignoreJids: ignoreJids }; + } ignoreJids.push(data.remoteJid); } else { - ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid); + this.logger.debug('[ignoreJid] -> Removendo remoteJid da lista de ignorados...'); + ignoreJids = ignoreJids.filter((jid: string) => jid !== data.remoteJid); } + this.logger.debug('[ignoreJid] -> Atualizando configurações no banco...'); const updateSettings = await this.settingsRepository.update({ where: { id: settings.id, @@ -939,32 +1147,44 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }, }); + this.logger.debug(`[ignoreJid] -> ignoreJids atualizada: ${JSON.stringify(updateSettings.ignoreJids)}`); return { ignoreJids: updateSettings.ignoreJids, }; } catch (error) { - this.logger.error(error); + this.logger.error(`[ignoreJid] -> Erro ao alterar lista de ignorados: ${error}`); throw new Error('Error setting default settings'); } } // Emit public async emit({ instance, remoteJid, msg, pushName }: EmitData) { - if (!this.integrationEnabled) return; + this.logger.debug(`[emit] -> Iniciando método com instance: ${JSON.stringify(instance)}, remoteJid: ${remoteJid}, msg: ${JSON.stringify(msg)}, pushName: ${pushName}`); + if (!this.integrationEnabled) { + this.logger.debug('[emit] -> OpenAI está desabilitado, encerrando execução.'); + return; + } try { + this.logger.debug('[emit] -> Buscando configurações da instância...'); const settings = await this.settingsRepository.findFirst({ where: { instanceId: instance.instanceId, }, }); - if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return; + if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) { + this.logger.debug(`[emit] -> remoteJid ${remoteJid} está na lista de ignorados, encerrando execução.`); + return; + } + this.logger.debug('[emit] -> Buscando sessão atual do remoteJid...'); let session = await this.getSession(remoteJid, instance); const content = getConversationMessage(msg); + this.logger.debug(`[emit] -> Conteúdo da mensagem: ${content}`); + this.logger.debug('[emit] -> Verificando se existe bot que atenda ao trigger...'); let findBot = (await this.findBotTrigger( this.botRepository, this.settingsRepository, @@ -974,6 +1194,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro )) as OpenaiBot; if (!findBot) { + this.logger.debug('[emit] -> Nenhum bot encontrado pelo trigger, checando fallback...'); const fallback = await this.settingsRepository.findFirst({ where: { instanceId: instance.instanceId, @@ -981,6 +1202,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }); if (fallback?.openaiIdFallback) { + this.logger.debug(`[emit] -> Fallback ID encontrado: ${fallback.openaiIdFallback}, tentando buscar bot fallback...`); const findFallback = await this.botRepository.findFirst({ where: { id: fallback.openaiIdFallback, @@ -989,31 +1211,34 @@ export class OpenaiController extends ChatbotController implements ChatbotContro findBot = findFallback; } else { + this.logger.debug('[emit] -> Nenhum fallback configurado, encerrando execução.'); 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; + let { + expire, + keywordFinish, + delayMessage, + unknownMessage, + listeningFromMe, + stopBotFromMe, + keepOpen, + debounceTime, + ignoreJids, + splitMessages, + timePerChar, + } = findBot; - 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 (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; @@ -1025,6 +1250,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro }; if (stopBotFromMe && key.fromMe && session) { + this.logger.debug('[emit] -> Bot deve parar caso a mensagem venha de mim, atualizando status da sessão para "paused"...'); session = await this.sessionRepository.update({ where: { id: session.id, @@ -1036,16 +1262,22 @@ export class OpenaiController extends ChatbotController implements ChatbotContro } if (!listeningFromMe && key.fromMe) { + this.logger.debug('[emit] -> Bot não responde mensagens enviadas de mim, encerrando execução.'); return; } if (session && !session.awaitUser) { + this.logger.debug('[emit] -> Sessão existente, mas session.awaitUser é false, encerrando execução.'); return; } if (debounceTime && debounceTime > 0) { + this.logger.debug('[emit] -> Iniciando lógica de debounce...'); this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => { + this.logger.debug(`[emit - Debounce] -> Chamando serviço OpenAI com debouncedContent: ${debouncedContent}`); + if (findBot.botType === 'assistant') { + this.logger.debug('[emit - Debounce] -> Bot é do tipo "assistant", chamando processOpenaiAssistant...'); await this.openaiService.processOpenaiAssistant( this.waMonitor.waInstances[instance.instanceName], remoteJid, @@ -1072,6 +1304,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro } if (findBot.botType === 'chatCompletion') { + this.logger.debug('[emit - Debounce] -> Bot é do tipo "chatCompletion", chamando processOpenaiChatCompletion...'); await this.openaiService.processOpenaiChatCompletion( this.waMonitor.waInstances[instance.instanceName], remoteJid, @@ -1097,7 +1330,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro } }); } else { + this.logger.debug('[emit] -> Sem debounce, chamando serviço OpenAI imediatamente...'); + if (findBot.botType === 'assistant') { + this.logger.debug('[emit] -> Bot é do tipo "assistant", chamando processOpenaiAssistant...'); await this.openaiService.processOpenaiAssistant( this.waMonitor.waInstances[instance.instanceName], remoteJid, @@ -1111,6 +1347,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro } if (findBot.botType === 'chatCompletion') { + this.logger.debug('[emit] -> Bot é do tipo "chatCompletion", chamando processOpenaiChatCompletion...'); await this.openaiService.processOpenaiChatCompletion( this.waMonitor.waInstances[instance.instanceName], remoteJid, @@ -1123,9 +1360,10 @@ export class OpenaiController extends ChatbotController implements ChatbotContro } } + this.logger.debug('[emit] -> Execução finalizada com sucesso.'); return; } catch (error) { - this.logger.error(error); + this.logger.error(`[emit] -> Erro geral no fluxo: ${error}`); return; } } diff --git a/src/api/integrations/chatbot/openai/services/openai.service.ts b/src/api/integrations/chatbot/openai/services/openai.service.ts index 440e3941..e1f90360 100644 --- a/src/api/integrations/chatbot/openai/services/openai.service.ts +++ b/src/api/integrations/chatbot/openai/services/openai.service.ts @@ -25,7 +25,11 @@ export class OpenaiService { private readonly logger = new Logger('OpenaiService'); private async sendMessageToBot(instance: any, openaiBot: OpenaiBot, remoteJid: string, content: string) { + this.logger.debug('Enviando mensagem para o bot (sendMessageToBot).'); + this.logger.debug(`RemoteJid: ${remoteJid}, Content: ${content}`); + const systemMessages: any = openaiBot.systemMessages; + this.logger.debug(`SystemMessages recuperadas: ${systemMessages}`); const messagesSystem: any[] = systemMessages.map((message) => { return { @@ -35,6 +39,7 @@ export class OpenaiService { }); const assistantMessages: any = openaiBot.assistantMessages; + this.logger.debug(`AssistantMessages recuperadas: ${assistantMessages}`); const messagesAssistant: any[] = assistantMessages.map((message) => { return { @@ -44,6 +49,7 @@ export class OpenaiService { }); const userMessages: any = openaiBot.userMessages; + this.logger.debug(`UserMessages recuperadas: ${userMessages}`); const messagesUser: any[] = userMessages.map((message) => { return { @@ -58,9 +64,11 @@ export class OpenaiService { }; if (this.isImageMessage(content)) { + this.logger.debug('Identificada mensagem de imagem no texto.'); const contentSplit = content.split('|'); const url = contentSplit[1].split('?')[0]; + this.logger.debug(`URL da imagem extraída: ${url}`); messageData.content = [ { type: 'text', text: contentSplit[2] || content }, @@ -74,22 +82,29 @@ export class OpenaiService { } const messages: any[] = [...messagesSystem, ...messagesAssistant, ...messagesUser, messageData]; + // o logo precisa formatar messages em joson + this.logger.debug(`Mensagens que serão enviadas para a API da OpenAI: ${JSON.stringify(messages)}`); if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (composing).'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } + this.logger.debug('Chamando a API da OpenAI (chat.completions.create).'); const completions = await this.client.chat.completions.create({ model: openaiBot.model, messages: messages, max_tokens: openaiBot.maxTokens, }); - if (instance.integration === Integration.WHATSAPP_BAILEYS) + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (paused).'); await instance.client.sendPresenceUpdate('paused', remoteJid); + } const message = completions.choices[0].message.content; + this.logger.debug(`Resposta obtida da OpenAI (sendMessageToBot): ${message}`); return message; } @@ -103,15 +118,20 @@ export class OpenaiService { content: string, threadId: string, ) { + this.logger.debug('Enviando mensagem para o assistente (sendMessageToAssistant).'); + this.logger.debug(`RemoteJid: ${remoteJid}, ThreadId: ${threadId}, Content: ${content}`); + const messageData: any = { role: fromMe ? 'assistant' : 'user', content: [{ type: 'text', text: content }], }; if (this.isImageMessage(content)) { + this.logger.debug('Identificada mensagem de imagem no texto para Assistant.'); const contentSplit = content.split('|'); const url = contentSplit[1].split('?')[0]; + this.logger.debug(`URL da imagem extraída: ${url}`); messageData.content = [ { type: 'text', text: contentSplit[2] || content }, @@ -124,28 +144,36 @@ export class OpenaiService { ]; } + this.logger.debug('Criando mensagem no thread do Assistant.'); await this.client.beta.threads.messages.create(threadId, messageData); if (fromMe) { + this.logger.debug('Mensagem enviada foi do próprio bot (fromMe). Enviando Telemetry.'); sendTelemetry('/message/sendText'); return; } + this.logger.debug('Iniciando corrida (run) do Assistant com ID do assistant configurado.'); const runAssistant = await this.client.beta.threads.runs.create(threadId, { assistant_id: openaiBot.assistantId, }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (composing).'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } + this.logger.debug('Aguardando resposta do Assistant (getAIResponse).'); const response = await this.getAIResponse(threadId, runAssistant.id, openaiBot.functionUrl, remoteJid, pushName); - if (instance.integration === Integration.WHATSAPP_BAILEYS) + if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (paused).'); await instance.client.sendPresenceUpdate('paused', remoteJid); + } const message = response?.data[0].content[0].text.value; + this.logger.debug(`Resposta obtida do Assistant (sendMessageToAssistant): ${message}`); return message; } @@ -157,8 +185,10 @@ export class OpenaiService { settings: OpenaiSetting, message: string, ) { - const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; + this.logger.debug('Enviando mensagem para o WhatsApp (sendMessageWhatsapp).'); + this.logger.debug(`RemoteJid: ${remoteJid}, Mensagem: ${message}`); + const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; let textBuffer = ''; let lastIndex = 0; @@ -178,8 +208,11 @@ export class OpenaiService { return null; }; + // Processa links (ou mídia) dentro do texto + this.logger.debug('Verificando se a mensagem contém mídia (links) no formato [altText](url).'); while ((match = linkRegex.exec(message)) !== null) { const [fullMatch, exclMark, altText, url] = match; + this.logger.debug(`Match encontrado: ${fullMatch}, url: ${url}, altText: ${altText}`); const mediaType = getMediaType(url); const beforeText = message.slice(lastIndex, match.index); @@ -193,22 +226,24 @@ export class OpenaiService { const minDelay = 1000; const maxDelay = 20000; + // Envia primeiro o texto que estiver no buffer 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) { + this.logger.debug('Atualizando presença (composing) antes de enviar texto em partes.'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } await new Promise((resolve) => { setTimeout(async () => { + this.logger.debug(`Enviando texto (splitMessage): ${message}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -222,10 +257,12 @@ export class OpenaiService { }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença (paused) após enviar parte do texto.'); await instance.client.sendPresenceUpdate('paused', remoteJid); } } } else { + this.logger.debug(`Enviando texto inteiro do buffer: ${textBuffer.trim()}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -238,7 +275,9 @@ export class OpenaiService { } } + this.logger.debug(`Identificado arquivo de mídia do tipo: ${mediaType}`); if (mediaType === 'audio') { + this.logger.debug('Enviando arquivo de áudio para o WhatsApp.'); await instance.audioWhatsapp({ number: remoteJid.split('@')[0], delay: settings?.delayMessage || 1000, @@ -246,6 +285,7 @@ export class OpenaiService { caption: altText, }); } else { + this.logger.debug('Enviando arquivo de mídia (imagem, vídeo ou documento) para o WhatsApp.'); await instance.mediaMessage( { number: remoteJid.split('@')[0], @@ -259,12 +299,14 @@ export class OpenaiService { ); } } else { + this.logger.debug('Não é um tipo de mídia suportado. Adicionando link no buffer de texto.'); textBuffer += `[${altText}](${url})`; } lastIndex = linkRegex.lastIndex; } + // Processa o texto restante, caso exista if (lastIndex < message.length) { const remainingText = message.slice(lastIndex); if (remainingText.trim()) { @@ -277,22 +319,24 @@ export class OpenaiService { const minDelay = 1000; const maxDelay = 20000; + // Envia o que restou no textBuffer 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) { + this.logger.debug('Atualizando presença (composing) antes de enviar resto do texto em partes.'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } await new Promise((resolve) => { setTimeout(async () => { + this.logger.debug(`Enviando texto (splitMessage): ${message}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -306,10 +350,12 @@ export class OpenaiService { }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { + this.logger.debug('Atualizando presença (paused) após enviar parte final do texto.'); await instance.client.sendPresenceUpdate('paused', remoteJid); } } } else { + this.logger.debug(`Enviando todo o texto restante no buffer: ${textBuffer.trim()}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -322,8 +368,10 @@ export class OpenaiService { } } + this.logger.debug('Enviando telemetria após envio de texto.'); sendTelemetry('/message/sendText'); + this.logger.debug(`Atualizando sessão (id: ${session.id}) para 'opened' e 'awaitUser: true'.`); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -336,7 +384,13 @@ export class OpenaiService { } public async createAssistantNewSession(instance: InstanceDto, data: any) { - if (data.remoteJid === 'status@broadcast') return; + this.logger.debug('Iniciando criação de nova sessão do Assistant (createAssistantNewSession).'); + this.logger.debug(`Dados recebidos: ${JSON.stringify(data)}`); + + if (data.remoteJid === 'status@broadcast') { + this.logger.debug('remoteJid é status@broadcast, abortando criação de sessão.'); + return; + } const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { @@ -344,17 +398,24 @@ export class OpenaiService { }, }); - if (!creds) throw new Error('Openai Creds not found'); + if (!creds) { + this.logger.error('Openai Creds não encontrados, lançando erro.'); + throw new Error('Openai Creds not found'); + } try { + this.logger.debug('Instanciando cliente OpenAI para Assistant.'); this.client = new OpenAI({ apiKey: creds.apiKey, }); - const threadId = (await this.client.beta.threads.create({})).id; + this.logger.debug('Criando thread (beta.threads.create).'); + const thread = await this.client.beta.threads.create({}); + const threadId = thread.id; let session = null; if (threadId) { + this.logger.debug('Thread criada com sucesso. Salvando sessão no banco de dados.'); session = await this.prismaRepository.integrationSession.create({ data: { remoteJid: data.remoteJid, @@ -370,7 +431,7 @@ export class OpenaiService { } return { session }; } catch (error) { - this.logger.error(error); + this.logger.error(`Erro ao criar nova sessão do Assistant: ${error}`); return; } } @@ -385,6 +446,9 @@ export class OpenaiService { session: IntegrationSession, content: string, ) { + this.logger.debug('Iniciando sessão do Assistant (initAssistantNewSession).'); + this.logger.debug(`RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); + const data = await this.createAssistantNewSession(instance, { remoteJid, pushName, @@ -394,8 +458,10 @@ export class OpenaiService { if (data.session) { session = data.session; + this.logger.debug(`Sessão criada com sucesso. ID: ${session.id}`); } + this.logger.debug('Enviando mensagem para Assistant para iniciar conversa.'); const message = await this.sendMessageToAssistant( instance, openaiBot, @@ -406,7 +472,11 @@ export class OpenaiService { session.sessionId, ); - await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + this.logger.debug(`Retorno do Assistant: ${message}`); + if (message) { + this.logger.debug('Enviando mensagem do Assistant para WhatsApp.'); + await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + } return; } @@ -427,10 +497,12 @@ export class OpenaiService { remoteJid: string, pushName: string, ) { + this.logger.debug(`Consultando run do Assistant (getAIResponse). ThreadId: ${threadId}, RunId: ${runId}`); const getRun = await this.client.beta.threads.runs.retrieve(threadId, runId); let toolCalls; switch (getRun.status) { case 'requires_action': + this.logger.debug('Run requer ação (requires_action). Verificando chamadas de ferramenta (tool_calls).'); toolCalls = getRun?.required_action?.submit_tool_outputs?.tool_calls; if (toolCalls) { @@ -442,6 +514,7 @@ export class OpenaiService { : toolCall?.function?.arguments; let output = null; + this.logger.debug(`Chamando função externa: ${functionName} com argumentos:`, functionArgument); try { const { data } = await axios.post(functionUrl, { @@ -449,13 +522,16 @@ export class OpenaiService { arguments: { ...functionArgument, remoteJid, pushName }, }); + // Serializa saída para string output = JSON.stringify(data) .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); + this.logger.debug(`Resposta da função externa (${functionName}):`, data); } catch (error) { + this.logger.error(`Erro ao chamar função externa (${functionName}):`, error); output = JSON.stringify(error) .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') @@ -464,6 +540,7 @@ export class OpenaiService { .replace(/\t/g, '\\t'); } + this.logger.debug('Submetendo output para a run do Assistant (submitToolOutputs).'); await this.client.beta.threads.runs.submitToolOutputs(threadId, runId, { tool_outputs: [ { @@ -475,14 +552,18 @@ export class OpenaiService { } } + this.logger.debug('Repetindo chamada getAIResponse até status diferente de requires_action.'); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'queued': + this.logger.debug('Run está em fila (queued). Aguardando 1 segundo antes de tentar novamente.'); await new Promise((resolve) => setTimeout(resolve, 1000)); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'in_progress': + this.logger.debug('Run está em progresso (in_progress). Aguardando 1 segundo antes de tentar novamente.'); await new Promise((resolve) => setTimeout(resolve, 1000)); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'completed': + this.logger.debug('Run concluída (completed). Recuperando última mensagem.'); return await this.client.beta.threads.messages.list(threadId, { run_id: runId, limit: 1, @@ -504,21 +585,27 @@ export class OpenaiService { settings: OpenaiSetting, content: string, ) { + this.logger.debug('Processando mensagem para o Assistant (processOpenaiAssistant).'); + this.logger.debug( + `RemoteJid: ${remoteJid}, pushName: ${pushName}, fromMe: ${fromMe}, content: ${content}`, + ); + if (session && session.status === 'closed') { + this.logger.debug('A sessão está fechada, não será processada.'); return; } if (session && settings.expire && settings.expire > 0) { + this.logger.debug('Verificando tempo de expiração da sessão...'); 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) { + this.logger.debug(`Sessão expirada há ${diffInMinutes} minutos.`); if (settings.keepOpen) { + this.logger.debug('Atualizando status da sessão para CLOSED.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -528,6 +615,7 @@ export class OpenaiService { }, }); } else { + this.logger.debug('Deletando sessão do banco de dados.'); await this.prismaRepository.integrationSession.deleteMany({ where: { botId: openaiBot.id, @@ -536,6 +624,7 @@ export class OpenaiService { }); } + this.logger.debug('Recriando nova sessão de Assistant...'); await this.initAssistantNewSession( instance, remoteJid, @@ -551,11 +640,13 @@ export class OpenaiService { } if (!session) { + this.logger.debug('Nenhuma sessão ativa encontrada, criando nova sessão de Assistant...'); await this.initAssistantNewSession(instance, remoteJid, pushName, fromMe, openaiBot, settings, session, content); return; } - if (session.status !== 'paused') + if (session.status !== 'paused') { + this.logger.debug('Marcando sessão como aberta e awaitUser = false.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -565,9 +656,12 @@ export class OpenaiService { awaitUser: false, }, }); + } if (!content) { + this.logger.debug('Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); if (settings.unknownMessage) { + this.logger.debug(`Enviando unknownMessage para o remoteJid: ${remoteJid}`); this.waMonitor.waInstances[instance.instanceName].textMessage( { number: remoteJid.split('@')[0], @@ -576,13 +670,13 @@ export class OpenaiService { }, false, ); - sendTelemetry('/message/sendText'); } return; } if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { + this.logger.debug('Keyword finish detectada. Encerrando sessão.'); if (settings.keepOpen) { await this.prismaRepository.integrationSession.update({ where: { @@ -603,20 +697,25 @@ export class OpenaiService { return; } + this.logger.debug('Buscando OpenaiCreds no banco...'); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { id: openaiBot.openaiCredsId, }, }); - if (!creds) throw new Error('Openai Creds not found'); + if (!creds) { + this.logger.error('Openai Creds não encontrados, lançando erro.'); + throw new Error('Openai Creds not found'); + } + this.logger.debug('Instanciando cliente OpenAI para processar a mensagem no Assistant.'); this.client = new OpenAI({ apiKey: creds.apiKey, }); const threadId = session.sessionId; - + this.logger.debug(`Enviando mensagem ao Assistant (threadId: ${threadId}).`); const message = await this.sendMessageToAssistant( instance, openaiBot, @@ -627,15 +726,25 @@ export class OpenaiService { threadId, ); - await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + if (message) { + this.logger.debug(`Resposta do Assistant recebida. Enviando para WhatsApp: ${message}`); + await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + } return; } public async createChatCompletionNewSession(instance: InstanceDto, data: any) { - if (data.remoteJid === 'status@broadcast') return; + this.logger.debug('Iniciando criação de nova sessão de chatCompletion (createChatCompletionNewSession).'); + this.logger.debug(`Dados recebidos: ${JSON.stringify(data)}`); + + if (data.remoteJid === 'status@broadcast') { + this.logger.debug('remoteJid é status@broadcast, abortando criação de sessão.'); + return; + } const id = Math.floor(Math.random() * 10000000000).toString(); + this.logger.debug(`Gerando ID pseudo-aleatório da sessão: ${id}`); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { @@ -643,9 +752,13 @@ export class OpenaiService { }, }); - if (!creds) throw new Error('Openai Creds not found'); + if (!creds) { + this.logger.error('Openai Creds não encontrados, lançando erro.'); + throw new Error('Openai Creds not found'); + } try { + this.logger.debug('Criando sessão no banco de dados.'); const session = await this.prismaRepository.integrationSession.create({ data: { remoteJid: data.remoteJid, @@ -661,7 +774,7 @@ export class OpenaiService { return { session, creds }; } catch (error) { - this.logger.error(error); + this.logger.error(`Erro ao criar nova sessão de chatCompletion: ${error}`); return; } } @@ -675,6 +788,9 @@ export class OpenaiService { session: IntegrationSession, content: string, ) { + this.logger.debug('Iniciando sessão de chatCompletion (initChatCompletionNewSession).'); + this.logger.debug(`RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); + const data = await this.createChatCompletionNewSession(instance, { remoteJid, pushName, @@ -683,16 +799,21 @@ export class OpenaiService { }); session = data.session; - const creds = data.creds; + this.logger.debug(`Sessão criada com sucesso (ID: ${session.id}). Instanciando cliente OpenAI.`); this.client = new OpenAI({ apiKey: creds.apiKey, }); + this.logger.debug('Enviando mensagem para o Bot usando chatCompletion.'); const message = await this.sendMessageToBot(instance, openaiBot, remoteJid, content); - await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + this.logger.debug(`Resposta do Bot: ${message}`); + if (message) { + this.logger.debug('Enviando resposta para o WhatsApp.'); + await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + } return; } @@ -706,21 +827,27 @@ export class OpenaiService { settings: OpenaiSetting, content: string, ) { + this.logger.debug('Processando ChatCompletion (processOpenaiChatCompletion).'); + this.logger.debug( + `RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}, SessionId: ${session?.id}`, + ); + if (session && session.status !== 'opened') { + this.logger.debug('Sessão existente não está aberta. Não será processado.'); return; } if (session && settings.expire && settings.expire > 0) { + this.logger.debug('Verificando tempo de expiração da sessão...'); 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) { + this.logger.debug(`Sessão expirada há ${diffInMinutes} minutos.`); if (settings.keepOpen) { + this.logger.debug('Atualizando status da sessão para CLOSED.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -730,6 +857,7 @@ export class OpenaiService { }, }); } else { + this.logger.debug('Deletando sessão do banco de dados.'); await this.prismaRepository.integrationSession.deleteMany({ where: { botId: openaiBot.id, @@ -738,16 +866,19 @@ export class OpenaiService { }); } + this.logger.debug('Recriando nova sessão de chatCompletion...'); await this.initChatCompletionNewSession(instance, remoteJid, pushName, openaiBot, settings, session, content); return; } } if (!session) { + this.logger.debug('Nenhuma sessão encontrada. Criando nova sessão de chatCompletion...'); await this.initChatCompletionNewSession(instance, remoteJid, pushName, openaiBot, settings, session, content); return; } + this.logger.debug('Marcando sessão como aberta e awaitUser = false.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -759,7 +890,9 @@ export class OpenaiService { }); if (!content) { + this.logger.debug('Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); if (settings.unknownMessage) { + this.logger.debug(`Enviando unknownMessage para o remoteJid: ${remoteJid}`); this.waMonitor.waInstances[instance.instanceName].textMessage( { number: remoteJid.split('@')[0], @@ -768,13 +901,13 @@ export class OpenaiService { }, false, ); - sendTelemetry('/message/sendText'); } return; } if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { + this.logger.debug('Keyword finish detectada. Encerrando sessão.'); if (settings.keepOpen) { await this.prismaRepository.integrationSession.update({ where: { @@ -795,33 +928,47 @@ export class OpenaiService { return; } + this.logger.debug('Buscando OpenaiCreds no banco...'); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { id: openaiBot.openaiCredsId, }, }); - if (!creds) throw new Error('Openai Creds not found'); + if (!creds) { + this.logger.error('Openai Creds não encontrados, lançando erro.'); + throw new Error('Openai Creds not found'); + } + this.logger.debug('Instanciando cliente OpenAI para processar a mensagem (ChatCompletion).'); this.client = new OpenAI({ apiKey: creds.apiKey, }); + this.logger.debug('Enviando mensagem para o Bot usando chatCompletion.'); const message = await this.sendMessageToBot(instance, openaiBot, remoteJid, content); - await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + this.logger.debug(`Resposta do Bot: ${message}`); + if (message) { + this.logger.debug('Enviando resposta para o WhatsApp.'); + await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); + } return; } public async speechToText(creds: OpenaiCreds, msg: any, updateMediaMessage: any) { + this.logger.debug('Iniciando conversão de fala em texto (speechToText).'); + let audio; if (msg?.message?.mediaUrl) { + this.logger.debug('Baixando áudio via URL (mediaUrl).'); audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => { return Buffer.from(response.data, 'binary'); }); } else { + this.logger.debug('Baixando áudio via downloadMediaMessage (baileys).'); audio = await downloadMediaMessage( { key: msg.key, message: msg?.message }, 'buffer', @@ -836,13 +983,14 @@ export class OpenaiService { const lang = this.configService.get('LANGUAGE').includes('pt') ? 'pt' : this.configService.get('LANGUAGE'); + this.logger.debug(`Definindo idioma da transcrição como: ${lang}`); const formData = new FormData(); - formData.append('file', audio, 'audio.ogg'); formData.append('model', 'whisper-1'); formData.append('language', lang); + this.logger.debug('Enviando requisição POST para a API de transcrição do OpenAI.'); const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -850,6 +998,7 @@ export class OpenaiService { }, }); + this.logger.debug(`Status da requisição: ${response.status}`); return response?.data?.text; } } diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 146375cb..7c38a697 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -1,3 +1,4 @@ +import { Logger } from '@config/logger.config'; import { InstanceDto } from '@api/dto/instance.dto'; import { ProxyDto } from '@api/dto/proxy.dto'; import { SettingsDto } from '@api/dto/settings.dto'; @@ -10,7 +11,6 @@ import { PrismaRepository, Query } from '@api/repository/repository.service'; import { eventManager, waMonitor } from '@api/server.module'; import { Events, wa } from '@api/types/wa.types'; import { Auth, Chatwoot, ConfigService, HttpServer } from '@config/env.config'; -import { Logger } from '@config/logger.config'; import { NotFoundException } from '@exceptions'; import { Contact, Message } from '@prisma/client'; import { WASocket } from 'baileys'; @@ -51,6 +51,7 @@ export class ChannelStartupService { public difyService = new DifyService(waMonitor, this.configService, this.prismaRepository); public setInstance(instance: InstanceDto) { + this.logger.debug(`[setInstance] Definindo dados da instância: ${JSON.stringify(instance)}`); this.logger.setInstance(instance.instanceName); this.instance.name = instance.instanceName; @@ -61,6 +62,7 @@ export class ChannelStartupService { this.instance.businessId = instance.businessId; if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { + this.logger.debug('[setInstance] Enviando evento de STATUS_INSTANCE para Chatwoot'); this.chatwootService.eventWhatsapp( Events.STATUS_INSTANCE, { instanceName: this.instance.name }, @@ -73,6 +75,7 @@ export class ChannelStartupService { } public set instanceName(name: string) { + this.logger.debug(`[setter instanceName] Atribuindo: ${name}`); this.logger.setInstance(name); if (!name) { @@ -87,6 +90,7 @@ export class ChannelStartupService { } public set instanceId(id: string) { + this.logger.debug(`[setter instanceId] Atribuindo: ${id}`); if (!id) { this.instance.id = v4(); return; @@ -99,6 +103,7 @@ export class ChannelStartupService { } public set integration(integration: string) { + this.logger.debug(`[setter integration] Atribuindo: ${integration}`); this.instance.integration = integration; } @@ -107,6 +112,7 @@ export class ChannelStartupService { } public set number(number: string) { + this.logger.debug(`[setter number] Atribuindo número: ${number}`); this.instance.number = number; } @@ -115,6 +121,7 @@ export class ChannelStartupService { } public set token(token: string) { + this.logger.debug(`[setter token] Atribuindo token.`); this.instance.token = token; } @@ -127,6 +134,7 @@ export class ChannelStartupService { } public async loadWebhook() { + this.logger.debug(`[loadWebhook] Carregando webhook para instanceId: ${this.instanceId}`); const data = await this.prismaRepository.webhook.findUnique({ where: { instanceId: this.instanceId, @@ -135,9 +143,12 @@ export class ChannelStartupService { this.localWebhook.enabled = data?.enabled; this.localWebhook.webhookBase64 = data?.webhookBase64; + + this.logger.debug('[loadWebhook] Webhook carregado com sucesso.'); } public async loadSettings() { + this.logger.debug(`[loadSettings] Carregando configurações para instanceId: ${this.instanceId}`); const data = await this.prismaRepository.setting.findUnique({ where: { instanceId: this.instanceId, @@ -151,9 +162,12 @@ export class ChannelStartupService { this.localSettings.readMessages = data?.readMessages; this.localSettings.readStatus = data?.readStatus; this.localSettings.syncFullHistory = data?.syncFullHistory; + + this.logger.debug('[loadSettings] Configurações carregadas com sucesso.'); } public async setSettings(data: SettingsDto) { + this.logger.debug(`[setSettings] Atualizando configurações: ${JSON.stringify(data)}`); await this.prismaRepository.setting.upsert({ where: { instanceId: this.instanceId, @@ -186,9 +200,12 @@ export class ChannelStartupService { this.localSettings.readMessages = data?.readMessages; this.localSettings.readStatus = data?.readStatus; this.localSettings.syncFullHistory = data?.syncFullHistory; + + this.logger.debug('[setSettings] Configurações atualizadas com sucesso.'); } public async findSettings() { + this.logger.debug(`[findSettings] Buscando configurações para instanceId: ${this.instanceId}`); const data = await this.prismaRepository.setting.findUnique({ where: { instanceId: this.instanceId, @@ -196,9 +213,11 @@ export class ChannelStartupService { }); if (!data) { + this.logger.debug('[findSettings] Nenhuma configuração encontrada.'); return null; } + this.logger.debug('[findSettings] Configurações encontradas.'); return { rejectCall: data.rejectCall, msgCall: data.msgCall, @@ -211,7 +230,9 @@ export class ChannelStartupService { } public async loadChatwoot() { + this.logger.debug('[loadChatwoot] Carregando dados do Chatwoot...'); if (!this.configService.get('CHATWOOT').ENABLED) { + this.logger.debug('[loadChatwoot] Chatwoot não está habilitado nas configurações.'); return; } @@ -235,10 +256,14 @@ export class ChannelStartupService { this.localChatwoot.importContacts = data?.importContacts; this.localChatwoot.importMessages = data?.importMessages; this.localChatwoot.daysLimitImportMessages = data?.daysLimitImportMessages; + + this.logger.debug('[loadChatwoot] Dados do Chatwoot carregados com sucesso.'); } public async setChatwoot(data: ChatwootDto) { + this.logger.debug(`[setChatwoot] Atualizando dados do Chatwoot: ${JSON.stringify(data)}`); if (!this.configService.get('CHATWOOT').ENABLED) { + this.logger.debug('[setChatwoot] Chatwoot não está habilitado nas configurações.'); return; } @@ -275,8 +300,8 @@ export class ChannelStartupService { }); Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null }); - this.clearCacheChatwoot(); + this.logger.debug('[setChatwoot] Dados do Chatwoot atualizados com sucesso.'); return; } @@ -303,12 +328,14 @@ export class ChannelStartupService { }); Object.assign(this.localChatwoot, { ...data, signDelimiter: data.signMsg ? data.signDelimiter : null }); - this.clearCacheChatwoot(); + this.logger.debug('[setChatwoot] Dados do Chatwoot criados com sucesso.'); } public async findChatwoot(): Promise { + this.logger.debug(`[findChatwoot] Buscando dados do Chatwoot para instanceId: ${this.instanceId}`); if (!this.configService.get('CHATWOOT').ENABLED) { + this.logger.debug('[findChatwoot] Chatwoot não está habilitado nas configurações.'); return null; } @@ -319,11 +346,12 @@ export class ChannelStartupService { }); if (!data) { + this.logger.debug('[findChatwoot] Nenhum dado de Chatwoot encontrado.'); return null; } const ignoreJidsArray = Array.isArray(data.ignoreJids) ? data.ignoreJids.map((event) => String(event)) : []; - + this.logger.debug('[findChatwoot] Dados de Chatwoot encontrados com sucesso.'); return { enabled: data?.enabled, accountId: data.accountId, @@ -345,12 +373,15 @@ export class ChannelStartupService { } public clearCacheChatwoot() { + this.logger.debug('[clearCacheChatwoot] Limpando cache do Chatwoot...'); if (this.localChatwoot?.enabled) { this.chatwootService.getCache()?.deleteAll(this.instanceName); + this.logger.debug('[clearCacheChatwoot] Cache do Chatwoot limpo com sucesso.'); } } public async loadProxy() { + this.logger.debug(`[loadProxy] Carregando dados de proxy para instanceId: ${this.instanceId}`); this.localProxy.enabled = false; if (process.env.PROXY_HOST) { @@ -376,9 +407,12 @@ export class ChannelStartupService { this.localProxy.username = data?.username; this.localProxy.password = data?.password; } + + this.logger.debug('[loadProxy] Dados de proxy carregados com sucesso.'); } public async setProxy(data: ProxyDto) { + this.logger.debug(`[setProxy] Definindo dados de proxy: ${JSON.stringify(data)}`); await this.prismaRepository.proxy.upsert({ where: { instanceId: this.instanceId, @@ -403,9 +437,11 @@ export class ChannelStartupService { }); Object.assign(this.localProxy, data); + this.logger.debug('[setProxy] Dados de proxy atualizados com sucesso.'); } public async findProxy() { + this.logger.debug(`[findProxy] Buscando dados de proxy para instanceId: ${this.instanceId}`); const data = await this.prismaRepository.proxy.findUnique({ where: { instanceId: this.instanceId, @@ -413,20 +449,22 @@ export class ChannelStartupService { }); if (!data) { + this.logger.debug('[findProxy] Proxy não encontrado.'); throw new NotFoundException('Proxy not found'); } + this.logger.debug('[findProxy] Dados de proxy encontrados com sucesso.'); return data; } public async sendDataWebhook(event: Events, data: T, local = true) { + this.logger.debug(`[sendDataWebhook] Enviando dados de webhook. Evento: ${event}, local: ${local}`); const serverUrl = this.configService.get('SERVER').URL; const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds const localISOTime = new Date(Date.now() - tzoffset).toISOString(); const now = localISOTime; const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; - const instanceApikey = this.token || 'Apikey not found'; await eventManager.emit({ @@ -440,10 +478,12 @@ export class ChannelStartupService { apiKey: expose && instanceApikey ? instanceApikey : null, local, }); + this.logger.debug('[sendDataWebhook] Evento de webhook enviado com sucesso.'); } // Check if the number is MX or AR public formatMXOrARNumber(jid: string): string { + this.logger.debug(`[formatMXOrARNumber] Formatando número MX ou AR: ${jid}`); const countryCode = jid.substring(0, 2); if (Number(countryCode) === 52 || Number(countryCode) === 54) { @@ -451,7 +491,6 @@ export class ChannelStartupService { const number = countryCode + jid.substring(3); return number; } - return jid; } return jid; @@ -459,6 +498,7 @@ export class ChannelStartupService { // Check if the number is br public formatBRNumber(jid: string) { + this.logger.debug(`[formatBRNumber] Formatando número brasileiro: ${jid}`); const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); if (regexp.test(jid)) { const match = regexp.exec(jid); @@ -477,11 +517,14 @@ export class ChannelStartupService { } public createJid(number: string): string { + this.logger.debug(`[createJid] Criando JID para o número: ${number}`); if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { + this.logger.debug('[createJid] Retornando número pois já possui sufixo de grupo ou WhatsApp.'); return number; } if (number.includes('@broadcast')) { + this.logger.debug('[createJid] Retornando número pois já é um broadcast.'); return number; } @@ -495,6 +538,7 @@ export class ChannelStartupService { if (number.includes('-') && number.length >= 24) { number = number.replace(/[^\d-]/g, ''); + this.logger.debug('[createJid] Número identificado como grupo, adicionando @g.us.'); return `${number}@g.us`; } @@ -502,17 +546,19 @@ export class ChannelStartupService { if (number.length >= 18) { number = number.replace(/[^\d-]/g, ''); + this.logger.debug('[createJid] Número extenso, provavelmente grupo, adicionando @g.us.'); return `${number}@g.us`; } number = this.formatMXOrARNumber(number); - number = this.formatBRNumber(number); + this.logger.debug('[createJid] Adicionando sufixo @s.whatsapp.net para número individual.'); return `${number}@s.whatsapp.net`; } public async fetchContacts(query: Query) { + this.logger.debug(`[fetchContacts] Buscando contatos. Query: ${JSON.stringify(query)}`); const remoteJid = query?.where?.remoteJid ? query?.where?.remoteJid.includes('@') ? query.where?.remoteJid @@ -527,12 +573,15 @@ export class ChannelStartupService { where['remoteJid'] = remoteJid; } - return await this.prismaRepository.contact.findMany({ + const contacts = await this.prismaRepository.contact.findMany({ where, }); + this.logger.debug(`[fetchContacts] Retornando ${contacts.length} contato(s).`); + return contacts; } public async fetchMessages(query: Query) { + this.logger.debug(`[fetchMessages] Buscando mensagens. Query: ${JSON.stringify(query)}`); const keyFilters = query?.where?.key as { id?: string; fromMe?: boolean; @@ -599,6 +648,7 @@ export class ChannelStartupService { }, }); + this.logger.debug(`[fetchMessages] Total de mensagens encontradas: ${count}.`); return { messages: { total: count, @@ -610,7 +660,8 @@ export class ChannelStartupService { } public async fetchStatusMessage(query: any) { - return await this.prismaRepository.messageUpdate.findMany({ + this.logger.debug(`[fetchStatusMessage] Buscando status de mensagens. Query: ${JSON.stringify(query)}`); + const results = await this.prismaRepository.messageUpdate.findMany({ where: { instanceId: this.instanceId, remoteJid: query.where?.remoteJid, @@ -619,9 +670,12 @@ export class ChannelStartupService { skip: query.offset * (query?.page === 1 ? 0 : (query?.page as number) - 1), take: query.offset, }); + this.logger.debug(`[fetchStatusMessage] Retornando ${results.length} atualização(ões) de status.`); + return results; } public async fetchChats(query: any) { + this.logger.debug(`[fetchChats] Buscando chats. Query: ${JSON.stringify(query)}`); const remoteJid = query?.where?.remoteJid ? query?.where?.remoteJid.includes('@') ? query.where?.remoteJid @@ -632,77 +686,78 @@ export class ChannelStartupService { if (!remoteJid) { results = await this.prismaRepository.$queryRaw` - SELECT - "Chat"."id", - "Chat"."remoteJid", - "Chat"."name", - "Chat"."labels", - "Chat"."createdAt", - "Chat"."updatedAt", - "Contact"."pushName", - "Contact"."profilePicUrl", - "Chat"."unreadMessages", - (ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id, - (ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key, - (ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name, - (ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant, - (ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type, - (ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message, - (ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info, - (ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source, - (ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp, - (ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id, - (ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id, - (ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status - FROM "Chat" - LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid" - LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid" - WHERE - "Chat"."instanceId" = ${this.instanceId} - GROUP BY - "Chat"."id", - "Chat"."remoteJid", - "Contact"."id" - ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC; - `; + SELECT + "Chat"."id", + "Chat"."remoteJid", + "Chat"."name", + "Chat"."labels", + "Chat"."createdAt", + "Chat"."updatedAt", + "Contact"."pushName", + "Contact"."profilePicUrl", + "Chat"."unreadMessages", + (ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id, + (ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key, + (ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name, + (ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant, + (ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type, + (ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message, + (ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info, + (ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source, + (ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp, + (ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id, + (ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id, + (ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status + FROM "Chat" + LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid" + LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid" + WHERE + "Chat"."instanceId" = ${this.instanceId} + GROUP BY + "Chat"."id", + "Chat"."remoteJid", + "Contact"."id" + ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC; + `; } else { results = await this.prismaRepository.$queryRaw` - SELECT - "Chat"."id", - "Chat"."remoteJid", - "Chat"."name", - "Chat"."labels", - "Chat"."createdAt", - "Chat"."updatedAt", - "Contact"."pushName", - "Contact"."profilePicUrl", - "Chat"."unreadMessages", - (ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id, - (ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key, - (ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name, - (ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant, - (ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type, - (ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message, - (ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info, - (ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source, - (ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp, - (ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id, - (ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id, - (ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status - FROM "Chat" - LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid" - LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid" - WHERE - "Chat"."instanceId" = ${this.instanceId} AND "Chat"."remoteJid" = ${remoteJid} and "Message"."messageType" != 'reactionMessage' - GROUP BY - "Chat"."id", - "Chat"."remoteJid", - "Contact"."id" - ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC; - `; + SELECT + "Chat"."id", + "Chat"."remoteJid", + "Chat"."name", + "Chat"."labels", + "Chat"."createdAt", + "Chat"."updatedAt", + "Contact"."pushName", + "Contact"."profilePicUrl", + "Chat"."unreadMessages", + (ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id, + (ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key, + (ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name, + (ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant, + (ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type, + (ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message, + (ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info, + (ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source, + (ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp, + (ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id, + (ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id, + (ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status + FROM "Chat" + LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid" + LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid" + WHERE + "Chat"."instanceId" = ${this.instanceId} AND "Chat"."remoteJid" = ${remoteJid} and "Message"."messageType" != 'reactionMessage' + GROUP BY + "Chat"."id", + "Chat"."remoteJid", + "Contact"."id" + ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC; + `; } if (results && isArray(results) && results.length > 0) { + this.logger.debug(`[fetchChats] Retornando ${results.length} chat(s).`); return results.map((chat) => { return { id: chat.id, @@ -734,6 +789,7 @@ export class ChannelStartupService { }); } + this.logger.debug('[fetchChats] Nenhum chat encontrado.'); return []; } } diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index bc27db5c..806e8878 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -136,7 +136,7 @@ export class Logger { this.console(value, Type.WARN); } - public error(value: any) { + public error(value: any, p0?: { message: any; stack: any; }) { this.console(value, Type.ERROR); } @@ -144,7 +144,7 @@ export class Logger { this.console(value, Type.VERBOSE); } - public debug(value: any) { + public debug(value: any, p0?: { config: any; }) { this.console(value, Type.DEBUG); } From cd673107b8f9adaf344fed16fc89a627edb6d6c9 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 12 Jan 2025 20:30:09 -0500 Subject: [PATCH 2/3] Salvando-historico do usuario --- src/api/controllers/sendMessage.controller.ts | 151 +++++++- src/api/dto/sendMessage.dto.ts | 2 - .../evolution/evolution.channel.service.ts | 26 +- .../chatwoot/services/chatwoot.service.ts | 84 ++++- .../controllers/evolutionBot.controller.ts | 2 +- .../chatbot/openai/services/openai.service.ts | 346 +++++++++++------- 6 files changed, 459 insertions(+), 152 deletions(-) diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index ac40562c..77117c84 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -17,81 +17,200 @@ import { import { WAMonitoringService } from '@api/services/monitor.service'; import { BadRequestException } from '@exceptions'; import { isBase64, isURL } from 'class-validator'; +import { Logger } from '@config/logger.config'; export class SendMessageController { constructor(private readonly waMonitor: WAMonitoringService) {} + private readonly logger = new Logger('SendMessageController'); + public async sendTemplate({ instanceName }: InstanceDto, data: SendTemplateDto) { - return await this.waMonitor.waInstances[instanceName].templateMessage(data); + this.logger.log(`[sendTemplate] [${instanceName}] - Iniciando envio de template...`); + this.logger.debug(`[sendTemplate] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].templateMessage(data); + + this.logger.log(`[sendTemplate] [${instanceName}] - Envio concluído com sucesso.`); + this.logger.debug(`[sendTemplate] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { - return await this.waMonitor.waInstances[instanceName].textMessage(data); + this.logger.log(`[sendText] [${instanceName}] - Iniciando envio de texto...`); + this.logger.debug(`[sendText] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].textMessage(data); + + this.logger.log(`[sendText] [${instanceName}] - Envio concluído com sucesso.`); + this.logger.debug(`[sendText] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto, file?: any) { + this.logger.log(`[sendMedia] [${instanceName}] - Iniciando envio de mídia...`); + this.logger.debug(`[sendMedia] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + if (isBase64(data?.media) && !data?.fileName && data?.mediatype === 'document') { + this.logger.error( + `[sendMedia] [${instanceName}] - Falha: Para base64, é necessário informar o nome do arquivo.` + ); throw new BadRequestException('For base64 the file name must be informed.'); } if (file || isURL(data?.media) || isBase64(data?.media)) { - return await this.waMonitor.waInstances[instanceName].mediaMessage(data, file); + const result = await this.waMonitor.waInstances[instanceName].mediaMessage(data, file); + this.logger.log(`[sendMedia] [${instanceName}] - Envio de mídia concluído com sucesso.`); + this.logger.debug(`[sendMedia] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + return result; } + + this.logger.error( + `[sendMedia] [${instanceName}] - Falha: Mídia deve ser uma URL ou base64.` + ); throw new BadRequestException('Owned media must be a url or base64'); } public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) { + this.logger.log(`[sendPtv] [${instanceName}] - Iniciando envio de vídeo (PTV)...`); + this.logger.debug(`[sendPtv] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + if (file || isURL(data?.video) || isBase64(data?.video)) { - return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file); + const result = await this.waMonitor.waInstances[instanceName].ptvMessage(data, file); + this.logger.log(`[sendPtv] [${instanceName}] - Envio de vídeo (PTV) concluído com sucesso.`); + this.logger.debug(`[sendPtv] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + return result; } + + this.logger.error( + `[sendPtv] [${instanceName}] - Falha: Vídeo deve ser uma URL ou base64.` + ); throw new BadRequestException('Owned media must be a url or base64'); } public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) { + this.logger.log(`[sendSticker] [${instanceName}] - Iniciando envio de sticker...`); + this.logger.debug(`[sendSticker] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + if (file || isURL(data.sticker) || isBase64(data.sticker)) { - return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file); + const result = await this.waMonitor.waInstances[instanceName].mediaSticker(data, file); + this.logger.log(`[sendSticker] [${instanceName}] - Envio de sticker concluído com sucesso.`); + this.logger.debug(`[sendSticker] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + return result; } + + this.logger.error( + `[sendSticker] [${instanceName}] - Falha: Sticker deve ser uma URL ou base64.` + ); throw new BadRequestException('Owned media must be a url or base64'); } public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto, file?: any) { + this.logger.log(`[sendWhatsAppAudio] [${instanceName}] - Iniciando envio de áudio...`); + this.logger.debug(`[sendWhatsAppAudio] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + if (file?.buffer || isURL(data.audio) || isBase64(data.audio)) { - // Si file existe y tiene buffer, o si es una URL o Base64, continúa - return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data, file); + const result = await this.waMonitor.waInstances[instanceName].audioWhatsapp(data, file); + this.logger.log(`[sendWhatsAppAudio] [${instanceName}] - Envio de áudio concluído com sucesso.`); + this.logger.debug(`[sendWhatsAppAudio] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + return result; } else { - console.error('El archivo no tiene buffer o el audio no es una URL o Base64 válida'); - throw new BadRequestException('Owned media must be a url, base64, or valid file with buffer'); + this.logger.error( + `[sendWhatsAppAudio] [${instanceName}] - Falha: O arquivo não possui buffer, ou o áudio não é uma URL/base64 válida.` + ); + throw new BadRequestException( + 'Owned media must be a url, base64, or valid file with buffer' + ); } } public async sendButtons({ instanceName }: InstanceDto, data: SendButtonsDto) { - return await this.waMonitor.waInstances[instanceName].buttonMessage(data); + this.logger.log(`[sendButtons] [${instanceName}] - Iniciando envio de botões...`); + this.logger.debug(`[sendButtons] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].buttonMessage(data); + + this.logger.log(`[sendButtons] [${instanceName}] - Envio de botões concluído com sucesso.`); + this.logger.debug(`[sendButtons] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { - return await this.waMonitor.waInstances[instanceName].locationMessage(data); + this.logger.log(`[sendLocation] [${instanceName}] - Iniciando envio de localização...`); + this.logger.debug(`[sendLocation] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].locationMessage(data); + + this.logger.log(`[sendLocation] [${instanceName}] - Envio de localização concluído com sucesso.`); + this.logger.debug(`[sendLocation] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendList({ instanceName }: InstanceDto, data: SendListDto) { - return await this.waMonitor.waInstances[instanceName].listMessage(data); + this.logger.log(`[sendList] [${instanceName}] - Iniciando envio de lista...`); + this.logger.debug(`[sendList] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].listMessage(data); + + this.logger.log(`[sendList] [${instanceName}] - Envio de lista concluído com sucesso.`); + this.logger.debug(`[sendList] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { - return await this.waMonitor.waInstances[instanceName].contactMessage(data); + this.logger.log(`[sendContact] [${instanceName}] - Iniciando envio de contato...`); + this.logger.debug(`[sendContact] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].contactMessage(data); + + this.logger.log(`[sendContact] [${instanceName}] - Envio de contato concluído com sucesso.`); + this.logger.debug(`[sendContact] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { + this.logger.log(`[sendReaction] [${instanceName}] - Iniciando envio de reação...`); + this.logger.debug(`[sendReaction] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) { + this.logger.error(`[sendReaction] [${instanceName}] - Falha: "reaction" deve ser um emoji.`); throw new BadRequestException('"reaction" must be an emoji'); } - return await this.waMonitor.waInstances[instanceName].reactionMessage(data); + + const result = await this.waMonitor.waInstances[instanceName].reactionMessage(data); + + this.logger.log(`[sendReaction] [${instanceName}] - Envio de reação concluído com sucesso.`); + this.logger.debug(`[sendReaction] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { - return await this.waMonitor.waInstances[instanceName].pollMessage(data); + this.logger.log(`[sendPoll] [${instanceName}] - Iniciando envio de enquete (poll)...`); + this.logger.debug(`[sendPoll] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].pollMessage(data); + + this.logger.log(`[sendPoll] [${instanceName}] - Envio de enquete concluído com sucesso.`); + this.logger.debug(`[sendPoll] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto, file?: any) { - return await this.waMonitor.waInstances[instanceName].statusMessage(data, file); + this.logger.log(`[sendStatus] [${instanceName}] - Iniciando envio de Status...`); + this.logger.debug(`[sendStatus] [${instanceName}] - Dados de envio: ${JSON.stringify(data)}`); + + const result = await this.waMonitor.waInstances[instanceName].statusMessage(data, file); + + this.logger.log(`[sendStatus] [${instanceName}] - Envio de Status concluído com sucesso.`); + this.logger.debug(`[sendStatus] [${instanceName}] - Resultado: ${JSON.stringify(result)}`); + + return result; } } diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index 801db0fe..ab450cea 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -49,8 +49,6 @@ export class Metadata { export class SendTextDto extends Metadata { text: string; // Conteúdo do texto number: string; // WhatsApp ou 'webwidget:...' - channel?: string; // ex: 'Channel::WebWidget' ou outro - inbox_id?: number; // se quiser mandar explicitamente ID=3 delay?: number; quoted?: any; linkPreview?: boolean; diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 88c071ce..5fc5c7e1 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -347,7 +347,7 @@ export class EvolutionStartupService extends ChannelStartupService { } protected async sendMessageWithTyping( - number: string, + number: string, // remoteJid message: any, options?: Options, isIntegration = false, @@ -393,18 +393,34 @@ export class EvolutionStartupService extends ChannelStartupService { ); // debug message + this.logger.debug(`[sendMessageWithTyping] Mensagem a ser enviada de numero: ${number}`); this.logger.debug( `[sendMessageWithTyping] Mensagem a ser enviada: ${JSON.stringify(message)}`, ); let messageRaw: any = { - key: { fromMe: true, id: messageId, remoteJid: number, channel: message.channel, inbox_id: message.inbox_id }, + key: { + fromMe: true, + id: messageId, + remoteJid: number, + }, messageTimestamp: Math.round(new Date().getTime() / 1000), webhookUrl, source: 'unknown', instanceId: this.instanceId, status: status[1], }; + + // Salvando chatwootConversationId para mensagens webwidget + if (number && number.startsWith('webwidget:')) { + this.logger.debug('[sendMessageWithTyping] Detectado número webwidget...'); + const conversationIdStr = number.split(':')[1] || '0'; + const conversation_id = parseInt(conversationIdStr, 10); + messageRaw.source = 'web'; + messageRaw.chatwootConversationId = conversation_id; + } + // debug messageRaw + this.logger.debug(`[sendMessageWithTyping] messageRaw a ser enviada: ${number}`); this.logger.debug(`[sendMessageWithTyping] messageRaw a ser enviada: ${JSON.stringify(messageRaw)}`); // Verifica o tipo de mídia para compor a mensagem @@ -498,7 +514,7 @@ export class EvolutionStartupService extends ChannelStartupService { }); } - this.logger.debug('[sendMessageWithTyping] Salvando mensagem no Prisma...'); + this.logger.debug(`[sendMessageWithTyping] Salvando mensagem no Prisma: ${JSON.stringify(messageRaw)}`); await this.prismaRepository.message.create({ data: messageRaw, }); @@ -517,11 +533,9 @@ export class EvolutionStartupService extends ChannelStartupService { this.logger.debug(`[textMessage] Dados recebidos: ${JSON.stringify(data2)}`); const res = await this.sendMessageWithTyping( - data2.number, + data2.number, { conversation: data2.text, - channel: data2.channel, // passa channel aqui - inbox_id: data2.inbox_id, // e inbox_id aqui }, { delay: data2?.delay, diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index dd62e4dc..7669ed19 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1365,6 +1365,9 @@ public async createConversation(instance: InstanceDto, body: any) { await new Promise((resolve) => setTimeout(resolve, 500)); const client = await this.clientCw(instance); + const chatId = + body.conversation?.meta?.sender?.identifier || + body.conversation?.meta?.sender?.phone_number?.replace('+', ''); if (!client) { this.logger.warn('[receiveWebhook] Client não encontrado'); @@ -1381,6 +1384,82 @@ public async createConversation(instance: InstanceDto, body: any) { const keyToDelete = `${instance.instanceName}:createConversation-${body.meta.sender.identifier}`; this.cache.delete(keyToDelete); } + + // debugar intancia + this.logger.debug(`[receiveWebhook] Instance recebido: ${JSON.stringify(instance)}`); + + // Salva a mensagem do usuário no banco de dados + if (body.message_type === 'incoming' && body.event === 'message_created') { + this.logger.debug('[receiveWebhook] Salvando Mensagem do usuário'); + + // Id da conversa no Chatwoot + const conversationId = body.conversation?.id; + // Conteúdo textual da mensagem enviada pelo usuário (ex.: "fsddsfsdf") + const content = body.content; + + if (!conversationId || !content) { + this.logger.error('[receiveWebhook] Dados insuficientes para salvar a mensagem.'); + return; + } + + try { + // Monta os dados para inserir no Prisma + const dataToSave = { + key: { + // Você pode gerar um ID único como quiser, por exemplo: + id: String(body.id) || 'algum-id-unico', + remoteJid: `webwidget:${conversationId}`, + fromMe: false, + }, + message: { + // A parte textual da conversa + conversation: content, + }, + messageType: 'conversation', + source: 'unknown', // se for enum, ajuste conforme seu modelo + messageTimestamp: Math.floor(Date.now() / 1000), + + // Informações específicas do Chatwoot que podem ser úteis + chatwootMessageId: body.id, // ID da mensagem no Chatwoot + chatwootConversationId: conversationId, // ID da conversa no Chatwoot + chatwootInboxId: body.inbox?.id ?? null, // ID da inbox que recebeu a mensagem + chatwootContactInboxSourceId: body.conversation?.contact_inbox?.source_id ?? null, + chatwootIsRead: false, // ou true, dependendo da sua lógica + + //instace + Instance: { + connect: { name: instance.instanceName } + }, + + // Relacionamento com sua instância + instanceId: instance.instanceId, + + // Caso queira salvar o nome do usuário (pushName) + pushName: body.sender?.name ?? null, + + // Se quiser salvar o “participant”, pode usar “identifier” ou “phone_number” + participant: + body.sender?.identifier ?? + body.sender?.phone_number ?? + null, + + // Se você tiver algum status a aplicar + status: 'pending', // ou qualquer outro valor que faça sentido + }; + + this.logger.debug(`[receiveWebhook] Dados para salvar: ${JSON.stringify(dataToSave)}`); + + const savedMsg = await this.prismaRepository.message.create({ + data: dataToSave, + }); + + this.logger.debug(`[receiveWebhook] Mensagem do usuário salva: ${JSON.stringify(savedMsg)}`); + } catch (error) { + this.logger.error(`[receiveWebhook] Erro ao salvar a mensagem: ${error.message}`); + // Lógica adicional de erro, se necessário + } + } + if ( !body?.conversation || @@ -1436,9 +1515,6 @@ public async createConversation(instance: InstanceDto, body: any) { return { message: 'webwidget_incoming_ok' }; } - const chatId = - body.conversation?.meta?.sender?.identifier || - body.conversation?.meta?.sender?.phone_number?.replace('+', ''); const messageReceived = body.content ? body.content .replaceAll(/(? { return { @@ -38,9 +39,10 @@ export class OpenaiService { }; }); + // Assistant messages const assistantMessages: any = openaiBot.assistantMessages; - this.logger.debug(`AssistantMessages recuperadas: ${assistantMessages}`); - + this.logger.debug(`[sendMessageToBot] AssistantMessages recuperadas: ${assistantMessages}`); + const messagesAssistant: any[] = assistantMessages.map((message) => { return { role: 'assistant', @@ -48,8 +50,9 @@ export class OpenaiService { }; }); + // User messages const userMessages: any = openaiBot.userMessages; - this.logger.debug(`UserMessages recuperadas: ${userMessages}`); + this.logger.debug(`[sendMessageToBot] UserMessages recuperadas: ${userMessages}`); const messagesUser: any[] = userMessages.map((message) => { return { @@ -58,17 +61,18 @@ export class OpenaiService { }; }); + // Imagem messages const messageData: any = { role: 'user', - content: [{ type: 'text', text: content }], + content: content, }; if (this.isImageMessage(content)) { - this.logger.debug('Identificada mensagem de imagem no texto.'); + this.logger.debug('[sendMessageToBot] Identificada mensagem de imagem no texto.'); const contentSplit = content.split('|'); const url = contentSplit[1].split('?')[0]; - this.logger.debug(`URL da imagem extraída: ${url}`); + this.logger.debug(`[sendMessageToBot] URL da imagem extraída: ${url}`); messageData.content = [ { type: 'text', text: contentSplit[2] || content }, @@ -81,17 +85,111 @@ export class OpenaiService { ]; } - const messages: any[] = [...messagesSystem, ...messagesAssistant, ...messagesUser, messageData]; - // o logo precisa formatar messages em joson - this.logger.debug(`Mensagens que serão enviadas para a API da OpenAI: ${JSON.stringify(messages)}`); + // History Mensagens + // Define aqui o limite máximo de caracteres do histórico + const MAX_HISTORY_CHARS = 30000; + + /** + * Extrai o texto principal de um objeto de mensagem. + * @param msg Mensagem recebida do banco (tipo `Message`). + * @returns Texto extraído da mensagem ou um JSON.stringify como fallback. + */ + function extrairTextoDaMensagem(msg: Message): string { + // Se não houver conteúdo + if (!msg?.message) { + return ''; + } + + // Caso seja mensagem de texto simples + if (msg.message.conversation) { + return msg.message.conversation; + } + + // Caso seja extendedTextMessage + if (msg.message.extendedTextMessage?.text) { + return msg.message.extendedTextMessage.text; + } + + // Caso seja imagem com caption + if (msg.message.imageMessage?.caption) { + return msg.message.imageMessage.caption; + } + + // Fallback: retorna o objeto como JSON + return JSON.stringify(msg.message); + } + + let historyArray: any[] = []; + + if (remoteJid && remoteJid.startsWith('webwidget:')) { + // Extrai o ID da conversa a partir do remoteJid (ex: 'webwidget:12345') + const conversationId = remoteJid.split(':')[1] || '0'; + this.logger.debug(`[sendMessageToBot] RemoteJid é webwidget. Buscando histórico da conversa: ${conversationId}`); + + // Busca todas as mensagens, sem limite de quantidade + let conversationHistory = await this.prismaRepository.message.findMany({ + where: { + chatwootConversationId: parseInt(conversationId), + }, + orderBy: { + messageTimestamp: 'desc', + }, + }); + this.logger.debug(`[sendMessageToBot] Histórico da conversa recuperado: ${conversationHistory.length} mensagens`); + + if (conversationHistory.length > 0) { + // Inverte para ficar das mais antigas às mais recentes + conversationHistory = conversationHistory.reverse(); + + // Mapeia cada mensagem para uma linha (role + texto) + let lines = conversationHistory.map((msg) => { + const textoExtraido = extrairTextoDaMensagem(msg); + // Se a mensagem for "fromMe", consideramos como 'assistant'; senão, 'user' + const roleOpenAI = msg.key?.fromMe ? 'assistant' : 'user'; + return `${roleOpenAI}: ${textoExtraido}`; + }); + + // Monta o histórico inicial com todas as linhas + let conversationString = lines.join('\n'); + + // Se exceder o limite de caracteres, remover mensagens mais antigas + while (conversationString.length > MAX_HISTORY_CHARS && lines.length > 0) { + // Remove a primeira linha (mais antiga) do array + lines.shift(); + conversationString = lines.join('\n'); + } + + historyArray = [ + { + role: 'system', + content: `This is the conversation history so far:\n\n${conversationString}`, + } + ]; + } else { + // Caso não haja histórico + historyArray = [ + { + role: 'system', + content: 'Não há histórico de conversa ainda.', + } + ]; + } + + this.logger.debug(`[sendMessageToBot] HistoryMessages: ${JSON.stringify(historyArray)}`); + } + + // debug historyMessages + this.logger.debug(`[sendMessageToBot] HistoryMessages: ${JSON.stringify(historyArray)}`); + const messages: any[] = [...messagesSystem, ...messagesAssistant, ...messagesUser, ...historyArray, messageData]; + this.logger.debug(`[sendMessageToBot] Mensagens que serão enviadas para a API da OpenAI: ${JSON.stringify(messages)}`); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (composing).'); + this.logger.debug('[sendMessageToBot] Atualizando presença para WHATSAPP_BAILEYS (composing).'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } - this.logger.debug('Chamando a API da OpenAI (chat.completions.create).'); + this.logger.debug('[sendMessageToBot] Chamando a API da OpenAI (chat.completions.create).'); const completions = await this.client.chat.completions.create({ model: openaiBot.model, messages: messages, @@ -99,12 +197,12 @@ export class OpenaiService { }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (paused).'); + this.logger.debug('[sendMessageToBot] Atualizando presença para WHATSAPP_BAILEYS (paused).'); await instance.client.sendPresenceUpdate('paused', remoteJid); } const message = completions.choices[0].message.content; - this.logger.debug(`Resposta obtida da OpenAI (sendMessageToBot): ${message}`); + this.logger.debug(`[sendMessageToBot] Resposta obtida da OpenAI: ${message}`); return message; } @@ -118,8 +216,8 @@ export class OpenaiService { content: string, threadId: string, ) { - this.logger.debug('Enviando mensagem para o assistente (sendMessageToAssistant).'); - this.logger.debug(`RemoteJid: ${remoteJid}, ThreadId: ${threadId}, Content: ${content}`); + this.logger.debug('[sendMessageToAssistant] Enviando mensagem para o assistente.'); + this.logger.debug(`[sendMessageToAssistant] RemoteJid: ${remoteJid}, ThreadId: ${threadId}, Content: ${content}`); const messageData: any = { role: fromMe ? 'assistant' : 'user', @@ -127,11 +225,11 @@ export class OpenaiService { }; if (this.isImageMessage(content)) { - this.logger.debug('Identificada mensagem de imagem no texto para Assistant.'); + this.logger.debug('[sendMessageToAssistant] Identificada mensagem de imagem no texto.'); const contentSplit = content.split('|'); const url = contentSplit[1].split('?')[0]; - this.logger.debug(`URL da imagem extraída: ${url}`); + this.logger.debug(`[sendMessageToAssistant] URL da imagem extraída: ${url}`); messageData.content = [ { type: 'text', text: contentSplit[2] || content }, @@ -144,36 +242,36 @@ export class OpenaiService { ]; } - this.logger.debug('Criando mensagem no thread do Assistant.'); + this.logger.debug('[sendMessageToAssistant] Criando mensagem no thread do Assistant.'); await this.client.beta.threads.messages.create(threadId, messageData); if (fromMe) { - this.logger.debug('Mensagem enviada foi do próprio bot (fromMe). Enviando Telemetry.'); + this.logger.debug('[sendMessageToAssistant] Mensagem enviada foi do próprio bot (fromMe). Enviando Telemetry.'); sendTelemetry('/message/sendText'); return; } - this.logger.debug('Iniciando corrida (run) do Assistant com ID do assistant configurado.'); + this.logger.debug('[sendMessageToAssistant] Iniciando corrida (run) do Assistant com ID do assistant configurado.'); const runAssistant = await this.client.beta.threads.runs.create(threadId, { assistant_id: openaiBot.assistantId, }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (composing).'); + this.logger.debug('[sendMessageToAssistant] Atualizando presença para WHATSAPP_BAILEYS (composing).'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } - this.logger.debug('Aguardando resposta do Assistant (getAIResponse).'); + this.logger.debug('[sendMessageToAssistant] Aguardando resposta do Assistant (getAIResponse).'); const response = await this.getAIResponse(threadId, runAssistant.id, openaiBot.functionUrl, remoteJid, pushName); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença para WHATSAPP_BAILEYS (paused).'); + this.logger.debug('[sendMessageToAssistant] Atualizando presença para WHATSAPP_BAILEYS (paused).'); await instance.client.sendPresenceUpdate('paused', remoteJid); } const message = response?.data[0].content[0].text.value; - this.logger.debug(`Resposta obtida do Assistant (sendMessageToAssistant): ${message}`); + this.logger.debug(`[sendMessageToAssistant] Resposta obtida do Assistant: ${message}`); return message; } @@ -185,8 +283,8 @@ export class OpenaiService { settings: OpenaiSetting, message: string, ) { - this.logger.debug('Enviando mensagem para o WhatsApp (sendMessageWhatsapp).'); - this.logger.debug(`RemoteJid: ${remoteJid}, Mensagem: ${message}`); + this.logger.debug('[sendMessageWhatsapp] Enviando mensagem para o WhatsApp.'); + this.logger.debug(`[sendMessageWhatsapp] RemoteJid: ${remoteJid}, Mensagem: ${message}`); const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; let textBuffer = ''; @@ -209,10 +307,10 @@ export class OpenaiService { }; // Processa links (ou mídia) dentro do texto - this.logger.debug('Verificando se a mensagem contém mídia (links) no formato [altText](url).'); + this.logger.debug('[sendMessageWhatsapp] Verificando se a mensagem contém mídia (links) no formato [altText](url).'); while ((match = linkRegex.exec(message)) !== null) { const [fullMatch, exclMark, altText, url] = match; - this.logger.debug(`Match encontrado: ${fullMatch}, url: ${url}, altText: ${altText}`); + this.logger.debug(`[sendMessageWhatsapp] Match encontrado: ${fullMatch}, url: ${url}, altText: ${altText}`); const mediaType = getMediaType(url); const beforeText = message.slice(lastIndex, match.index); @@ -236,14 +334,14 @@ export class OpenaiService { const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença (composing) antes de enviar texto em partes.'); + this.logger.debug('[sendMessageWhatsapp] Atualizando presença (composing) antes de enviar texto em partes.'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } await new Promise((resolve) => { setTimeout(async () => { - this.logger.debug(`Enviando texto (splitMessage): ${message}`); + this.logger.debug(`[sendMessageWhatsapp] Enviando texto (splitMessage): ${part}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -257,12 +355,12 @@ export class OpenaiService { }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença (paused) após enviar parte do texto.'); + this.logger.debug('[sendMessageWhatsapp] Atualizando presença (paused) após enviar parte do texto.'); await instance.client.sendPresenceUpdate('paused', remoteJid); } } } else { - this.logger.debug(`Enviando texto inteiro do buffer: ${textBuffer.trim()}`); + this.logger.debug(`[sendMessageWhatsapp] Enviando texto inteiro do buffer: ${textBuffer.trim()}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -275,9 +373,9 @@ export class OpenaiService { } } - this.logger.debug(`Identificado arquivo de mídia do tipo: ${mediaType}`); + this.logger.debug(`[sendMessageWhatsapp] Identificado arquivo de mídia do tipo: ${mediaType}`); if (mediaType === 'audio') { - this.logger.debug('Enviando arquivo de áudio para o WhatsApp.'); + this.logger.debug('[sendMessageWhatsapp] Enviando arquivo de áudio para o WhatsApp.'); await instance.audioWhatsapp({ number: remoteJid.split('@')[0], delay: settings?.delayMessage || 1000, @@ -285,7 +383,7 @@ export class OpenaiService { caption: altText, }); } else { - this.logger.debug('Enviando arquivo de mídia (imagem, vídeo ou documento) para o WhatsApp.'); + this.logger.debug('[sendMessageWhatsapp] Enviando arquivo de mídia (imagem, vídeo ou documento) para o WhatsApp.'); await instance.mediaMessage( { number: remoteJid.split('@')[0], @@ -299,7 +397,7 @@ export class OpenaiService { ); } } else { - this.logger.debug('Não é um tipo de mídia suportado. Adicionando link no buffer de texto.'); + this.logger.debug('[sendMessageWhatsapp] Não é um tipo de mídia suportado. Adicionando link no buffer de texto.'); textBuffer += `[${altText}](${url})`; } @@ -329,14 +427,14 @@ export class OpenaiService { const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença (composing) antes de enviar resto do texto em partes.'); + this.logger.debug('[sendMessageWhatsapp] Atualizando presença (composing) antes de enviar resto do texto em partes.'); await instance.client.presenceSubscribe(remoteJid); await instance.client.sendPresenceUpdate('composing', remoteJid); } await new Promise((resolve) => { setTimeout(async () => { - this.logger.debug(`Enviando texto (splitMessage): ${message}`); + this.logger.debug(`[sendMessageWhatsapp] Enviando texto (splitMessage): ${part}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -350,12 +448,12 @@ export class OpenaiService { }); if (instance.integration === Integration.WHATSAPP_BAILEYS) { - this.logger.debug('Atualizando presença (paused) após enviar parte final do texto.'); + this.logger.debug('[sendMessageWhatsapp] Atualizando presença (paused) após enviar parte final do texto.'); await instance.client.sendPresenceUpdate('paused', remoteJid); } } } else { - this.logger.debug(`Enviando todo o texto restante no buffer: ${textBuffer.trim()}`); + this.logger.debug(`[sendMessageWhatsapp] Enviando todo o texto restante no buffer: ${textBuffer.trim()}`); await instance.textMessage( { number: remoteJid.split('@')[0], @@ -368,10 +466,10 @@ export class OpenaiService { } } - this.logger.debug('Enviando telemetria após envio de texto.'); + this.logger.debug('[sendMessageWhatsapp] Enviando telemetria após envio de texto.'); sendTelemetry('/message/sendText'); - this.logger.debug(`Atualizando sessão (id: ${session.id}) para 'opened' e 'awaitUser: true'.`); + this.logger.debug(`[sendMessageWhatsapp] Atualizando sessão (id: ${session.id}) para 'opened' e 'awaitUser: true'.`); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -384,11 +482,11 @@ export class OpenaiService { } public async createAssistantNewSession(instance: InstanceDto, data: any) { - this.logger.debug('Iniciando criação de nova sessão do Assistant (createAssistantNewSession).'); - this.logger.debug(`Dados recebidos: ${JSON.stringify(data)}`); + this.logger.debug('[createAssistantNewSession] Iniciando criação de nova sessão do Assistant.'); + this.logger.debug(`[createAssistantNewSession] Dados recebidos: ${JSON.stringify(data)}`); if (data.remoteJid === 'status@broadcast') { - this.logger.debug('remoteJid é status@broadcast, abortando criação de sessão.'); + this.logger.debug('[createAssistantNewSession] remoteJid é status@broadcast, abortando criação de sessão.'); return; } @@ -399,23 +497,23 @@ export class OpenaiService { }); if (!creds) { - this.logger.error('Openai Creds não encontrados, lançando erro.'); + this.logger.error('[createAssistantNewSession] Openai Creds não encontrados, lançando erro.'); throw new Error('Openai Creds not found'); } try { - this.logger.debug('Instanciando cliente OpenAI para Assistant.'); + this.logger.debug('[createAssistantNewSession] Instanciando cliente OpenAI para Assistant.'); this.client = new OpenAI({ apiKey: creds.apiKey, }); - this.logger.debug('Criando thread (beta.threads.create).'); + this.logger.debug('[createAssistantNewSession] Criando thread (beta.threads.create).'); const thread = await this.client.beta.threads.create({}); const threadId = thread.id; let session = null; if (threadId) { - this.logger.debug('Thread criada com sucesso. Salvando sessão no banco de dados.'); + this.logger.debug('[createAssistantNewSession] Thread criada com sucesso. Salvando sessão no banco de dados.'); session = await this.prismaRepository.integrationSession.create({ data: { remoteJid: data.remoteJid, @@ -431,7 +529,7 @@ export class OpenaiService { } return { session }; } catch (error) { - this.logger.error(`Erro ao criar nova sessão do Assistant: ${error}`); + this.logger.error(`[createAssistantNewSession] Erro ao criar nova sessão do Assistant: ${error}`); return; } } @@ -446,8 +544,8 @@ export class OpenaiService { session: IntegrationSession, content: string, ) { - this.logger.debug('Iniciando sessão do Assistant (initAssistantNewSession).'); - this.logger.debug(`RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); + this.logger.debug('[initAssistantNewSession] Iniciando sessão do Assistant.'); + this.logger.debug(`[initAssistantNewSession] RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); const data = await this.createAssistantNewSession(instance, { remoteJid, @@ -458,10 +556,10 @@ export class OpenaiService { if (data.session) { session = data.session; - this.logger.debug(`Sessão criada com sucesso. ID: ${session.id}`); + this.logger.debug(`[initAssistantNewSession] Sessão criada com sucesso. ID: ${session.id}`); } - this.logger.debug('Enviando mensagem para Assistant para iniciar conversa.'); + this.logger.debug('[initAssistantNewSession] Enviando mensagem para Assistant para iniciar conversa.'); const message = await this.sendMessageToAssistant( instance, openaiBot, @@ -472,9 +570,9 @@ export class OpenaiService { session.sessionId, ); - this.logger.debug(`Retorno do Assistant: ${message}`); + this.logger.debug(`[initAssistantNewSession] Retorno do Assistant: ${message}`); if (message) { - this.logger.debug('Enviando mensagem do Assistant para WhatsApp.'); + this.logger.debug('[initAssistantNewSession] Enviando mensagem do Assistant para WhatsApp.'); await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); } @@ -497,12 +595,12 @@ export class OpenaiService { remoteJid: string, pushName: string, ) { - this.logger.debug(`Consultando run do Assistant (getAIResponse). ThreadId: ${threadId}, RunId: ${runId}`); + this.logger.debug(`[getAIResponse] Consultando run do Assistant. ThreadId: ${threadId}, RunId: ${runId}`); const getRun = await this.client.beta.threads.runs.retrieve(threadId, runId); let toolCalls; switch (getRun.status) { case 'requires_action': - this.logger.debug('Run requer ação (requires_action). Verificando chamadas de ferramenta (tool_calls).'); + this.logger.debug('[getAIResponse] Run requer ação. Verificando chamadas de ferramenta (tool_calls).'); toolCalls = getRun?.required_action?.submit_tool_outputs?.tool_calls; if (toolCalls) { @@ -514,7 +612,7 @@ export class OpenaiService { : toolCall?.function?.arguments; let output = null; - this.logger.debug(`Chamando função externa: ${functionName} com argumentos:`, functionArgument); + this.logger.debug(`[getAIResponse] Chamando função externa: ${functionName} com argumentos:`, functionArgument); try { const { data } = await axios.post(functionUrl, { @@ -529,9 +627,9 @@ export class OpenaiService { .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); - this.logger.debug(`Resposta da função externa (${functionName}):`, data); + this.logger.debug(`[getAIResponse] Resposta da função externa (${functionName}):`, data); } catch (error) { - this.logger.error(`Erro ao chamar função externa (${functionName}):`, error); + this.logger.error(`[getAIResponse] Erro ao chamar função externa (${functionName}):`, error); output = JSON.stringify(error) .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') @@ -540,7 +638,7 @@ export class OpenaiService { .replace(/\t/g, '\\t'); } - this.logger.debug('Submetendo output para a run do Assistant (submitToolOutputs).'); + this.logger.debug('[getAIResponse] Submetendo output para a run do Assistant (submitToolOutputs).'); await this.client.beta.threads.runs.submitToolOutputs(threadId, runId, { tool_outputs: [ { @@ -552,18 +650,18 @@ export class OpenaiService { } } - this.logger.debug('Repetindo chamada getAIResponse até status diferente de requires_action.'); + this.logger.debug('[getAIResponse] Repetindo chamada getAIResponse até status diferente de requires_action.'); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'queued': - this.logger.debug('Run está em fila (queued). Aguardando 1 segundo antes de tentar novamente.'); + this.logger.debug('[getAIResponse] Run está em fila (queued). Aguardando 1 segundo antes de tentar novamente.'); await new Promise((resolve) => setTimeout(resolve, 1000)); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'in_progress': - this.logger.debug('Run está em progresso (in_progress). Aguardando 1 segundo antes de tentar novamente.'); + this.logger.debug('[getAIResponse] Run está em progresso (in_progress). Aguardando 1 segundo antes de tentar novamente.'); await new Promise((resolve) => setTimeout(resolve, 1000)); return this.getAIResponse(threadId, runId, functionUrl, remoteJid, pushName); case 'completed': - this.logger.debug('Run concluída (completed). Recuperando última mensagem.'); + this.logger.debug('[getAIResponse] Run concluída (completed). Recuperando última mensagem.'); return await this.client.beta.threads.messages.list(threadId, { run_id: runId, limit: 1, @@ -585,27 +683,27 @@ export class OpenaiService { settings: OpenaiSetting, content: string, ) { - this.logger.debug('Processando mensagem para o Assistant (processOpenaiAssistant).'); + this.logger.debug('[processOpenaiAssistant] Processando mensagem para o Assistant.'); this.logger.debug( - `RemoteJid: ${remoteJid}, pushName: ${pushName}, fromMe: ${fromMe}, content: ${content}`, + `[processOpenaiAssistant] RemoteJid: ${remoteJid}, pushName: ${pushName}, fromMe: ${fromMe}, content: ${content}`, ); if (session && session.status === 'closed') { - this.logger.debug('A sessão está fechada, não será processada.'); + this.logger.debug('[processOpenaiAssistant] A sessão está fechada, não será processada.'); return; } if (session && settings.expire && settings.expire > 0) { - this.logger.debug('Verificando tempo de expiração da sessão...'); + this.logger.debug('[processOpenaiAssistant] Verificando tempo de expiração da sessão...'); 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) { - this.logger.debug(`Sessão expirada há ${diffInMinutes} minutos.`); + this.logger.debug(`[processOpenaiAssistant] Sessão expirada há ${diffInMinutes} minutos.`); if (settings.keepOpen) { - this.logger.debug('Atualizando status da sessão para CLOSED.'); + this.logger.debug('[processOpenaiAssistant] Atualizando status da sessão para CLOSED.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -615,7 +713,7 @@ export class OpenaiService { }, }); } else { - this.logger.debug('Deletando sessão do banco de dados.'); + this.logger.debug('[processOpenaiAssistant] Deletando sessão do banco de dados.'); await this.prismaRepository.integrationSession.deleteMany({ where: { botId: openaiBot.id, @@ -624,7 +722,7 @@ export class OpenaiService { }); } - this.logger.debug('Recriando nova sessão de Assistant...'); + this.logger.debug('[processOpenaiAssistant] Recriando nova sessão de Assistant...'); await this.initAssistantNewSession( instance, remoteJid, @@ -640,13 +738,13 @@ export class OpenaiService { } if (!session) { - this.logger.debug('Nenhuma sessão ativa encontrada, criando nova sessão de Assistant...'); + this.logger.debug('[processOpenaiAssistant] Nenhuma sessão ativa encontrada, criando nova sessão de Assistant...'); await this.initAssistantNewSession(instance, remoteJid, pushName, fromMe, openaiBot, settings, session, content); return; } if (session.status !== 'paused') { - this.logger.debug('Marcando sessão como aberta e awaitUser = false.'); + this.logger.debug('[processOpenaiAssistant] Marcando sessão como aberta e awaitUser = false.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -659,9 +757,9 @@ export class OpenaiService { } if (!content) { - this.logger.debug('Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); + this.logger.debug('[processOpenaiAssistant] Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); if (settings.unknownMessage) { - this.logger.debug(`Enviando unknownMessage para o remoteJid: ${remoteJid}`); + this.logger.debug(`[processOpenaiAssistant] Enviando unknownMessage para o remoteJid: ${remoteJid}`); this.waMonitor.waInstances[instance.instanceName].textMessage( { number: remoteJid.split('@')[0], @@ -676,7 +774,7 @@ export class OpenaiService { } if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { - this.logger.debug('Keyword finish detectada. Encerrando sessão.'); + this.logger.debug('[processOpenaiAssistant] Keyword finish detectada. Encerrando sessão.'); if (settings.keepOpen) { await this.prismaRepository.integrationSession.update({ where: { @@ -697,7 +795,7 @@ export class OpenaiService { return; } - this.logger.debug('Buscando OpenaiCreds no banco...'); + this.logger.debug('[processOpenaiAssistant] Buscando OpenaiCreds no banco...'); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { id: openaiBot.openaiCredsId, @@ -705,17 +803,17 @@ export class OpenaiService { }); if (!creds) { - this.logger.error('Openai Creds não encontrados, lançando erro.'); + this.logger.error('[processOpenaiAssistant] Openai Creds não encontrados, lançando erro.'); throw new Error('Openai Creds not found'); } - this.logger.debug('Instanciando cliente OpenAI para processar a mensagem no Assistant.'); + this.logger.debug('[processOpenaiAssistant] Instanciando cliente OpenAI para processar a mensagem no Assistant.'); this.client = new OpenAI({ apiKey: creds.apiKey, }); const threadId = session.sessionId; - this.logger.debug(`Enviando mensagem ao Assistant (threadId: ${threadId}).`); + this.logger.debug(`[processOpenaiAssistant] Enviando mensagem ao Assistant (threadId: ${threadId}).`); const message = await this.sendMessageToAssistant( instance, openaiBot, @@ -727,7 +825,7 @@ export class OpenaiService { ); if (message) { - this.logger.debug(`Resposta do Assistant recebida. Enviando para WhatsApp: ${message}`); + this.logger.debug(`[processOpenaiAssistant] Resposta do Assistant recebida. Enviando para WhatsApp: ${message}`); await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); } @@ -735,16 +833,16 @@ export class OpenaiService { } public async createChatCompletionNewSession(instance: InstanceDto, data: any) { - this.logger.debug('Iniciando criação de nova sessão de chatCompletion (createChatCompletionNewSession).'); - this.logger.debug(`Dados recebidos: ${JSON.stringify(data)}`); + this.logger.debug('[createChatCompletionNewSession] Iniciando criação de nova sessão de chatCompletion.'); + this.logger.debug(`[createChatCompletionNewSession] Dados recebidos: ${JSON.stringify(data)}`); if (data.remoteJid === 'status@broadcast') { - this.logger.debug('remoteJid é status@broadcast, abortando criação de sessão.'); + this.logger.debug('[createChatCompletionNewSession] remoteJid é status@broadcast, abortando criação de sessão.'); return; } const id = Math.floor(Math.random() * 10000000000).toString(); - this.logger.debug(`Gerando ID pseudo-aleatório da sessão: ${id}`); + this.logger.debug(`[createChatCompletionNewSession] Gerando ID pseudo-aleatório da sessão: ${id}`); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { @@ -753,12 +851,12 @@ export class OpenaiService { }); if (!creds) { - this.logger.error('Openai Creds não encontrados, lançando erro.'); + this.logger.error('[createChatCompletionNewSession] Openai Creds não encontrados, lançando erro.'); throw new Error('Openai Creds not found'); } try { - this.logger.debug('Criando sessão no banco de dados.'); + this.logger.debug('[createChatCompletionNewSession] Criando sessão no banco de dados.'); const session = await this.prismaRepository.integrationSession.create({ data: { remoteJid: data.remoteJid, @@ -774,7 +872,7 @@ export class OpenaiService { return { session, creds }; } catch (error) { - this.logger.error(`Erro ao criar nova sessão de chatCompletion: ${error}`); + this.logger.error(`[createChatCompletionNewSession] Erro ao criar nova sessão de chatCompletion: ${error}`); return; } } @@ -788,8 +886,8 @@ export class OpenaiService { session: IntegrationSession, content: string, ) { - this.logger.debug('Iniciando sessão de chatCompletion (initChatCompletionNewSession).'); - this.logger.debug(`RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); + this.logger.debug('[initChatCompletionNewSession] Iniciando sessão de chatCompletion.'); + this.logger.debug(`[initChatCompletionNewSession] RemoteJid: ${remoteJid}, PushName: ${pushName}, Content: ${content}`); const data = await this.createChatCompletionNewSession(instance, { remoteJid, @@ -800,18 +898,18 @@ export class OpenaiService { session = data.session; const creds = data.creds; - this.logger.debug(`Sessão criada com sucesso (ID: ${session.id}). Instanciando cliente OpenAI.`); + this.logger.debug(`[initChatCompletionNewSession] Sessão criada com sucesso (ID: ${session.id}). Instanciando cliente OpenAI.`); this.client = new OpenAI({ apiKey: creds.apiKey, }); - this.logger.debug('Enviando mensagem para o Bot usando chatCompletion.'); + this.logger.debug('[initChatCompletionNewSession] Enviando mensagem para o Bot usando chatCompletion.'); const message = await this.sendMessageToBot(instance, openaiBot, remoteJid, content); - this.logger.debug(`Resposta do Bot: ${message}`); + this.logger.debug(`[initChatCompletionNewSession] Resposta do Bot: ${message}`); if (message) { - this.logger.debug('Enviando resposta para o WhatsApp.'); + this.logger.debug('[initChatCompletionNewSession] Enviando resposta para o WhatsApp.'); await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); } @@ -833,21 +931,21 @@ export class OpenaiService { ); if (session && session.status !== 'opened') { - this.logger.debug('Sessão existente não está aberta. Não será processado.'); + this.logger.debug('[processOpenaiChatCompletion] Sessão existente não está aberta. Não será processado.'); return; } if (session && settings.expire && settings.expire > 0) { - this.logger.debug('Verificando tempo de expiração da sessão...'); + this.logger.debug('[processOpenaiChatCompletion] Verificando tempo de expiração da sessão...'); 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) { - this.logger.debug(`Sessão expirada há ${diffInMinutes} minutos.`); + this.logger.debug(`[processOpenaiChatCompletion] Sessão expirada há ${diffInMinutes} minutos.`); if (settings.keepOpen) { - this.logger.debug('Atualizando status da sessão para CLOSED.'); + this.logger.debug('[processOpenaiChatCompletion] Atualizando status da sessão para CLOSED.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -857,7 +955,7 @@ export class OpenaiService { }, }); } else { - this.logger.debug('Deletando sessão do banco de dados.'); + this.logger.debug('[processOpenaiChatCompletion] Deletando sessão do banco de dados.'); await this.prismaRepository.integrationSession.deleteMany({ where: { botId: openaiBot.id, @@ -866,19 +964,19 @@ export class OpenaiService { }); } - this.logger.debug('Recriando nova sessão de chatCompletion...'); + this.logger.debug('[processOpenaiChatCompletion] Recriando nova sessão de chatCompletion...'); await this.initChatCompletionNewSession(instance, remoteJid, pushName, openaiBot, settings, session, content); return; } } if (!session) { - this.logger.debug('Nenhuma sessão encontrada. Criando nova sessão de chatCompletion...'); + this.logger.debug('[processOpenaiChatCompletion] Nenhuma sessão encontrada. Criando nova sessão de chatCompletion...'); await this.initChatCompletionNewSession(instance, remoteJid, pushName, openaiBot, settings, session, content); return; } - this.logger.debug('Marcando sessão como aberta e awaitUser = false.'); + this.logger.debug('[processOpenaiChatCompletion] Marcando sessão como aberta e awaitUser = false.'); await this.prismaRepository.integrationSession.update({ where: { id: session.id, @@ -890,9 +988,9 @@ export class OpenaiService { }); if (!content) { - this.logger.debug('Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); + this.logger.debug('[processOpenaiChatCompletion] Não há conteúdo na mensagem. Verificando se existe unknownMessage para retorno.'); if (settings.unknownMessage) { - this.logger.debug(`Enviando unknownMessage para o remoteJid: ${remoteJid}`); + this.logger.debug(`[processOpenaiChatCompletion] Enviando unknownMessage para o remoteJid: ${remoteJid}`); this.waMonitor.waInstances[instance.instanceName].textMessage( { number: remoteJid.split('@')[0], @@ -907,7 +1005,7 @@ export class OpenaiService { } if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) { - this.logger.debug('Keyword finish detectada. Encerrando sessão.'); + this.logger.debug('[processOpenaiChatCompletion] Keyword finish detectada. Encerrando sessão.'); if (settings.keepOpen) { await this.prismaRepository.integrationSession.update({ where: { @@ -928,7 +1026,7 @@ export class OpenaiService { return; } - this.logger.debug('Buscando OpenaiCreds no banco...'); + this.logger.debug('[processOpenaiChatCompletion] Buscando OpenaiCreds no banco...'); const creds = await this.prismaRepository.openaiCreds.findFirst({ where: { id: openaiBot.openaiCredsId, @@ -936,21 +1034,21 @@ export class OpenaiService { }); if (!creds) { - this.logger.error('Openai Creds não encontrados, lançando erro.'); + this.logger.error('[processOpenaiChatCompletion] Openai Creds não encontrados, lançando erro.'); throw new Error('Openai Creds not found'); } - this.logger.debug('Instanciando cliente OpenAI para processar a mensagem (ChatCompletion).'); + this.logger.debug('[processOpenaiChatCompletion] Instanciando cliente OpenAI para processar a mensagem (ChatCompletion).'); this.client = new OpenAI({ apiKey: creds.apiKey, }); - this.logger.debug('Enviando mensagem para o Bot usando chatCompletion.'); + this.logger.debug('[processOpenaiChatCompletion] Enviando mensagem para o Bot usando chatCompletion.'); const message = await this.sendMessageToBot(instance, openaiBot, remoteJid, content); - this.logger.debug(`Resposta do Bot: ${message}`); + this.logger.debug(`[processOpenaiChatCompletion] Resposta do Bot: ${message}`); if (message) { - this.logger.debug('Enviando resposta para o WhatsApp.'); + this.logger.debug('[processOpenaiChatCompletion] Enviando resposta para o WhatsApp.'); await this.sendMessageWhatsapp(instance, session, remoteJid, settings, message); } @@ -958,17 +1056,17 @@ export class OpenaiService { } public async speechToText(creds: OpenaiCreds, msg: any, updateMediaMessage: any) { - this.logger.debug('Iniciando conversão de fala em texto (speechToText).'); + this.logger.debug('[speechToText] Iniciando conversão de fala em texto.'); let audio; if (msg?.message?.mediaUrl) { - this.logger.debug('Baixando áudio via URL (mediaUrl).'); + this.logger.debug('[speechToText] Baixando áudio via URL (mediaUrl).'); audio = await axios.get(msg.message.mediaUrl, { responseType: 'arraybuffer' }).then((response) => { return Buffer.from(response.data, 'binary'); }); } else { - this.logger.debug('Baixando áudio via downloadMediaMessage (baileys).'); + this.logger.debug('[speechToText] Baixando áudio via downloadMediaMessage (baileys).'); audio = await downloadMediaMessage( { key: msg.key, message: msg?.message }, 'buffer', @@ -983,14 +1081,14 @@ export class OpenaiService { const lang = this.configService.get('LANGUAGE').includes('pt') ? 'pt' : this.configService.get('LANGUAGE'); - this.logger.debug(`Definindo idioma da transcrição como: ${lang}`); + this.logger.debug(`[speechToText] Definindo idioma da transcrição como: ${lang}`); const formData = new FormData(); formData.append('file', audio, 'audio.ogg'); formData.append('model', 'whisper-1'); formData.append('language', lang); - this.logger.debug('Enviando requisição POST para a API de transcrição do OpenAI.'); + this.logger.debug('[speechToText] Enviando requisição POST para a API de transcrição do OpenAI.'); const response = await axios.post('https://api.openai.com/v1/audio/transcriptions', formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -998,7 +1096,7 @@ export class OpenaiService { }, }); - this.logger.debug(`Status da requisição: ${response.status}`); + this.logger.debug(`[speechToText] Status da requisição: ${response.status}`); return response?.data?.text; } } From 2f0e4ee1e5a816d85455a1c7ff1dbf25153b227e Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 12 Jan 2025 20:56:23 -0500 Subject: [PATCH 3/3] docker --- docker-compose.yaml | 48 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index b286919c..405d8f1b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,57 +1,61 @@ services: - api: - container_name: evolution_api - image: atendai/evolution-api:homolog + evolution-api: + container_name: evolution-api + build: + context: . + dockerfile: Dockerfile restart: always depends_on: - - redis + - evolution-redis - postgres ports: - 8080:8080 volumes: - - evolution_instances:/evolution/instances + - evolution-instances:/evolution/instances networks: - - evolution-net + - chatwoot-evolution-network env_file: - .env expose: - 8080 - redis: + evolution-redis: image: redis:latest + container_name: evolution-redis + restart: always networks: - - evolution-net - container_name: redis + - chatwoot-evolution-network command: > redis-server --port 6379 --appendonly yes volumes: - - evolution_redis:/data + - evolution-redis:/data ports: - 6379:6379 - postgres: - container_name: postgres + evolution-postgres: + container_name: evolution-postgres image: postgres:15 networks: - - evolution-net - command: ["postgres", "-c", "max_connections=1000"] + - chatwoot-evolution-network + command: [ "postgres", "-c", "max_connections=1000" ] restart: always ports: - 5432:5432 environment: - - POSTGRES_PASSWORD=PASSWORD + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} volumes: - - postgres_data:/var/lib/postgresql/data + - evolution-postgres:/var/lib/postgresql/data expose: - 5432 volumes: - evolution_instances: - evolution_redis: - postgres_data: + evolution-instances: + evolution-redis: + evolution-postgres: networks: - evolution-net: - name: evolution-net - driver: bridge + chatwoot-evolution-network: + external: true