diff --git a/.cursor/rules/specialized-rules/integration-channel-rules.mdc b/.cursor/rules/specialized-rules/integration-channel-rules.mdc index d2b94d96..bb61990b 100644 --- a/.cursor/rules/specialized-rules/integration-channel-rules.mdc +++ b/.cursor/rules/specialized-rules/integration-channel-rules.mdc @@ -49,10 +49,6 @@ export class ChannelController { return new BusinessStartupService(/* dependencies */); } - if (instanceData.integration === Integration.EVOLUTION) { - return new EvolutionStartupService(/* dependencies */); - } - if (instanceData.integration === Integration.WHATSAPP_BAILEYS) { return new BaileysStartupService(/* dependencies */); } @@ -62,43 +58,6 @@ export class ChannelController { } ``` -### Extended Channel Controller -```typescript -export class EvolutionController extends ChannelController implements ChannelControllerInterface { - private readonly logger = new Logger('EvolutionController'); - - constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { - super(prismaRepository, waMonitor); - } - - integrationEnabled: boolean; - - public async receiveWebhook(data: any) { - const numberId = data.numberId; - - if (!numberId) { - this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found'); - return; - } - - const instance = await this.prismaRepository.instance.findFirst({ - where: { number: numberId }, - }); - - if (!instance) { - this.logger.error('WebhookService -> receiveWebhook -> instance not found'); - return; - } - - await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); - - return { - status: 'success', - }; - } -} -``` - ## Channel Service Pattern ### Base Channel Service @@ -154,64 +113,6 @@ export class ChannelStartupService { } ``` -### Extended Channel Service -```typescript -export class EvolutionStartupService extends ChannelStartupService { - constructor( - configService: ConfigService, - eventEmitter: EventEmitter2, - prismaRepository: PrismaRepository, - cache: CacheService, - chatwootCache: CacheService, - ) { - super(configService, eventEmitter, prismaRepository, cache, chatwootCache); - } - - public async sendMessage(data: SendTextDto): Promise { - // Evolution-specific message sending logic - const response = await this.evolutionApiCall('/send-message', data); - return response; - } - - public async connectToWhatsapp(data: any): Promise { - // Evolution-specific connection logic - this.logger.log('Connecting to Evolution API'); - - // Set up webhook listeners - this.setupWebhookHandlers(); - - // Initialize connection - await this.initializeConnection(data); - } - - private async evolutionApiCall(endpoint: string, data: any): Promise { - const config = this.configService.get('EVOLUTION'); - - try { - const response = await axios.post(`${config.API_URL}${endpoint}`, data, { - headers: { - 'Authorization': `Bearer ${this.instance.token}`, - 'Content-Type': 'application/json', - }, - }); - - return response.data; - } catch (error) { - this.logger.error(`Evolution API call failed: ${error.message}`); - throw new InternalServerErrorException('Evolution API call failed'); - } - } - - private setupWebhookHandlers(): void { - // Set up webhook event handlers - } - - private async initializeConnection(data: any): Promise { - // Initialize connection with Evolution API - } -} -``` - ## Business API Service Pattern ### Meta Business Service @@ -431,29 +332,12 @@ export class ChannelRouter { constructor(configService: any, ...guards: any[]) { this.router = Router(); - this.router.use('/', new EvolutionRouter(configService).router); this.router.use('/', new MetaRouter(configService).router); this.router.use('/baileys', new BaileysRouter(...guards).router); } } ``` -### Specific Channel Router -```typescript -export class EvolutionRouter extends RouterBroker { - constructor(private readonly configService: ConfigService) { - super(); - this.router - .post(this.routerPath('webhook'), async (req, res) => { - const response = await evolutionController.receiveWebhook(req.body); - return res.status(HttpStatus.OK).json(response); - }); - } - - public readonly router: Router = Router(); -} -``` - ## Integration Types ### Channel Data Types @@ -471,7 +355,6 @@ type ChannelDataType = { export enum Integration { WHATSAPP_BUSINESS = 'WHATSAPP-BUSINESS', WHATSAPP_BAILEYS = 'WHATSAPP-BAILEYS', - EVOLUTION = 'EVOLUTION', } ``` @@ -502,51 +385,4 @@ public async sendMessage(data: SendTextDto): Promise { ## Channel Testing Pattern -### Channel Service Testing -```typescript -describe('EvolutionStartupService', () => { - let service: EvolutionStartupService; - let configService: jest.Mocked; - let eventEmitter: jest.Mocked; - - beforeEach(() => { - const mockConfig = { - get: jest.fn().mockReturnValue({ - API_URL: 'https://api.evolution.com', - }), - }; - - service = new EvolutionStartupService( - mockConfig as any, - eventEmitter, - prismaRepository, - cache, - chatwootCache, - ); - }); - - describe('sendMessage', () => { - it('should send message successfully', async () => { - const data = { number: '5511999999999', text: 'Test message' }; - - // Mock axios response - jest.spyOn(axios, 'post').mockResolvedValue({ - data: { success: true, messageId: '123' }, - }); - - const result = await service.sendMessage(data); - - expect(result.success).toBe(true); - expect(axios.post).toHaveBeenCalledWith( - expect.stringContaining('/send-message'), - data, - expect.objectContaining({ - headers: expect.objectContaining({ - 'Authorization': expect.stringContaining('Bearer'), - }), - }) - ); - }); - }); -}); -``` \ No newline at end of file +Tests for channel services should mock dependencies and verify behavior for message sending, connection handling, and error cases. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 143e6c74..9061a682 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ This document provides comprehensive guidelines for AI agents (Claude, GPT, Curs - `api/services/` – Business logic (core functionality) - `api/routes/` – Express route definitions (RouterBroker pattern) - `api/integrations/` – External service integrations - - `channel/` – WhatsApp providers (Baileys, Business API, Evolution) + - `channel/` – WhatsApp providers (Baileys, Business API) - `chatbot/` – AI/Bot integrations (OpenAI, Dify, Typebot, Chatwoot) - `event/` – Event systems (WebSocket, RabbitMQ, SQS, NATS, Pusher) - `storage/` – File storage (S3, MinIO) @@ -229,8 +229,7 @@ const result = await this.prismaRepository.instance.findUnique({ ### Channel Integration (WhatsApp Providers) - **Baileys**: WhatsApp Web with QR code authentication -- **Business API**: Official Meta WhatsApp Business API -- **Evolution API**: Custom WhatsApp integration +- **Business API**: Official Meta WhatsApp Business API - **Pattern**: Extend base channel service classes ### Chatbot Integration diff --git a/CLAUDE.md b/CLAUDE.md index 7c0e87a0..8c3d5cbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,6 @@ This file provides comprehensive guidance to Claude AI when working with the Evo **Evolution API** is a powerful, production-ready REST API for WhatsApp communication that supports multiple WhatsApp providers: - **Baileys** (WhatsApp Web) - Open-source WhatsApp Web client - **Meta Business API** - Official WhatsApp Business API -- **Evolution API** - Custom WhatsApp integration Built with **Node.js 20+**, **TypeScript 5+**, and **Express.js**, it provides extensive integrations with chatbots, CRM systems, and messaging platforms in a **multi-tenant architecture**. @@ -67,7 +66,7 @@ npm test # Run tests with watch mode ### Core Structure - **Multi-tenant SaaS**: Complete instance isolation with per-tenant authentication - **Multi-provider database**: PostgreSQL and MySQL via Prisma ORM with provider-specific schemas and migrations -- **WhatsApp integrations**: Baileys, Meta Business API, and Evolution API with unified interface +- **WhatsApp integrations**: Baileys and Meta Business API with unified interface - **Event-driven architecture**: EventEmitter2 for internal events + WebSocket, RabbitMQ, SQS, NATS, Pusher for external events - **Microservices pattern**: Modular integrations for chatbots, storage, and external services @@ -81,7 +80,7 @@ src/ │ ├── dto/ # Data Transfer Objects (simple classes) │ ├── guards/ # Authentication/authorization middleware │ ├── integrations/ # External service integrations -│ │ ├── channel/ # WhatsApp providers (Baileys, Business API, Evolution) +│ │ ├── channel/ # WhatsApp providers (Baileys, Business API) │ │ ├── chatbot/ # AI/Bot integrations (OpenAI, Dify, Typebot, Chatwoot) │ │ ├── event/ # Event systems (WebSocket, RabbitMQ, SQS, NATS, Pusher) │ │ └── storage/ # File storage (S3, MinIO) @@ -99,7 +98,6 @@ src/ **Channel Integrations** (`src/api/integrations/channel/`): - **Baileys**: WhatsApp Web client with QR code authentication - **Business API**: Official Meta WhatsApp Business API -- **Evolution API**: Custom WhatsApp integration - Connection lifecycle management per instance with automatic reconnection **Chatbot Integrations** (`src/api/integrations/chatbot/`): diff --git a/src/api/integrations/channel/channel.controller.ts b/src/api/integrations/channel/channel.controller.ts index 051be726..c189a193 100644 --- a/src/api/integrations/channel/channel.controller.ts +++ b/src/api/integrations/channel/channel.controller.ts @@ -8,7 +8,6 @@ import { ConfigService } from '@config/env.config'; import { BadRequestException } from '@exceptions'; import EventEmitter2 from 'eventemitter2'; -import { EvolutionStartupService } from './evolution/evolution.channel.service'; import { BusinessStartupService } from './meta/whatsapp.business.service'; import { BaileysStartupService } from './whatsapp/whatsapp.baileys.service'; @@ -68,16 +67,6 @@ export class ChannelController { ); } - if (instanceData.integration === Integration.EVOLUTION) { - return new EvolutionStartupService( - data.configService, - data.eventEmitter, - data.prismaRepository, - data.cache, - data.chatwootCache, - ); - } - if (instanceData.integration === Integration.WHATSAPP_BAILEYS) { return new BaileysStartupService( data.configService, diff --git a/src/api/integrations/channel/channel.router.ts b/src/api/integrations/channel/channel.router.ts index 5d878471..db5a83b4 100644 --- a/src/api/integrations/channel/channel.router.ts +++ b/src/api/integrations/channel/channel.router.ts @@ -1,6 +1,5 @@ import { Router } from 'express'; -import { EvolutionRouter } from './evolution/evolution.router'; import { MetaRouter } from './meta/meta.router'; import { BaileysRouter } from './whatsapp/baileys.router'; @@ -10,7 +9,6 @@ export class ChannelRouter { constructor(configService: any, ...guards: any[]) { this.router = Router(); - this.router.use('/', new EvolutionRouter(configService).router); this.router.use('/', new MetaRouter(configService).router); this.router.use('/baileys', new BaileysRouter(...guards).router); } diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts deleted file mode 100644 index 87bea08e..00000000 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ /dev/null @@ -1,888 +0,0 @@ -import { InstanceDto } from '@api/dto/instance.dto'; -import { - MediaMessage, - Options, - SendAudioDto, - SendButtonsDto, - SendMediaDto, - SendTextDto, -} from '@api/dto/sendMessage.dto'; -import * as s3Service from '@api/integrations/storage/s3/libs/minio.server'; -import { PrismaRepository } from '@api/repository/repository.service'; -import { chatbotController } from '@api/server.module'; -import { CacheService } from '@api/services/cache.service'; -import { ChannelStartupService } from '@api/services/channel.service'; -import { Events, wa } from '@api/types/wa.types'; -import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config'; -import { BadRequestException, InternalServerErrorException } from '@exceptions'; -import { createJid } from '@utils/createJid'; -import { sendTelemetry } from '@utils/sendTelemetry'; -import axios from 'axios'; -import { isBase64, isURL } from 'class-validator'; -import EventEmitter2 from 'eventemitter2'; -import FormData from 'form-data'; -import mimeTypes from 'mime-types'; -import { join } from 'path'; -import { v4 } from 'uuid'; - -export class EvolutionStartupService extends ChannelStartupService { - constructor( - public readonly configService: ConfigService, - public readonly eventEmitter: EventEmitter2, - public readonly prismaRepository: PrismaRepository, - public readonly cache: CacheService, - public readonly chatwootCache: CacheService, - ) { - super(configService, eventEmitter, prismaRepository, chatwootCache); - - this.client = null; - } - - public client: any; - - public stateConnection: wa.StateConnection = { state: 'open' }; - - public phoneNumber: string; - public mobile: boolean; - - public get connectionStatus() { - return this.stateConnection; - } - - public async closeClient() { - this.stateConnection = { state: 'close' }; - } - - public get qrCode(): wa.QrCode { - return { - pairingCode: this.instance.qrcode?.pairingCode, - code: this.instance.qrcode?.code, - base64: this.instance.qrcode?.base64, - count: this.instance.qrcode?.count, - }; - } - - public async logoutInstance() { - await this.closeClient(); - } - - public setInstance(instance: InstanceDto) { - this.logger.setInstance(instance.instanceId); - - this.instance.name = instance.instanceName; - this.instance.id = instance.instanceId; - this.instance.integration = instance.integration; - this.instance.number = instance.number; - this.instance.token = instance.token; - this.instance.businessId = instance.businessId; - - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { - instanceName: this.instance.name, - instanceId: this.instance.id, - integration: instance.integration, - }, - { - instance: this.instance.name, - status: 'created', - }, - ); - } - } - - public async profilePicture(number: string) { - const jid = createJid(number); - - return { - wuid: jid, - profilePictureUrl: null, - }; - } - - public async getProfileName() { - return null; - } - - public async profilePictureUrl() { - return null; - } - - public async getProfileStatus() { - return null; - } - - public async connectToWhatsapp(data?: any): Promise { - if (!data) { - this.loadChatwoot(); - return; - } - - try { - this.eventHandler(data); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } - - protected async eventHandler(received: any) { - try { - let messageRaw: any; - - if (received.message) { - const key = { - id: received.key.id || v4(), - remoteJid: received.key.remoteJid, - fromMe: received.key.fromMe, - profilePicUrl: received.profilePicUrl, - }; - messageRaw = { - key, - pushName: received.pushName, - message: received.message, - messageType: received.messageType, - messageTimestamp: Math.round(new Date().getTime() / 1000), - source: 'unknown', - instanceId: this.instanceId, - }; - - const isAudio = received?.message?.audioMessage; - - if (this.configService.get('OPENAI').ENABLED && isAudio) { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: this.instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); - - if ( - openAiDefaultSettings && - openAiDefaultSettings.openaiCredsId && - openAiDefaultSettings.speechToText && - received?.message?.audioMessage - ) { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`; - } - } - - this.logger.log(messageRaw); - - sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); - - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - await chatbotController.emit({ - 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) { - const chatwootSentMessage = await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw, - ); - - if (chatwootSentMessage?.id) { - messageRaw.chatwootMessageId = chatwootSentMessage.id; - messageRaw.chatwootInboxId = chatwootSentMessage.id; - messageRaw.chatwootConversationId = chatwootSentMessage.id; - } - } - - await this.prismaRepository.message.create({ - data: messageRaw, - }); - - await this.updateContact({ - remoteJid: messageRaw.key.remoteJid, - pushName: messageRaw.pushName, - profilePicUrl: received.profilePicUrl, - }); - } - } catch (error) { - this.logger.error(error); - } - } - - private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) { - const contactRaw: any = { - remoteJid: data.remoteJid, - pushName: data?.pushName, - instanceId: this.instanceId, - profilePicUrl: data?.profilePicUrl, - }; - - const existingContact = await this.prismaRepository.contact.findFirst({ - where: { - remoteJid: data.remoteJid, - instanceId: this.instanceId, - }, - }); - - if (existingContact) { - await this.prismaRepository.contact.updateMany({ - where: { - remoteJid: data.remoteJid, - instanceId: this.instanceId, - }, - data: contactRaw, - }); - } else { - await this.prismaRepository.contact.create({ - data: 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, - integration: this.instance.integration, - }, - 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 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, - file?: any, - isIntegration = false, - ) { - try { - let quoted: any; - let webhookUrl: any; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.key; - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - } - - if (options.delay) { - await new Promise((resolve) => setTimeout(resolve, options.delay)); - } - - if (options?.webhookUrl) { - webhookUrl = options.webhookUrl; - } - - let audioFile; - - const messageId = v4(); - - let messageRaw: any; - - if (message?.mediaType === 'image') { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - base64: isBase64(message.media) ? message.media : null, - mediaUrl: isURL(message.media) ? message.media : null, - quoted, - }, - messageType: 'imageMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } else if (message?.mediaType === 'video') { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - base64: isBase64(message.media) ? message.media : null, - mediaUrl: isURL(message.media) ? message.media : null, - quoted, - }, - messageType: 'videoMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } else if (message?.mediaType === 'audio') { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - base64: isBase64(message.media) ? message.media : null, - mediaUrl: isURL(message.media) ? message.media : null, - quoted, - }, - messageType: 'audioMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - - const buffer = Buffer.from(message.media, 'base64'); - audioFile = { - buffer, - mimetype: 'audio/mp4', - originalname: `${messageId}.mp4`, - }; - } else if (message?.mediaType === 'document') { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - base64: isBase64(message.media) ? message.media : null, - mediaUrl: isURL(message.media) ? message.media : null, - quoted, - }, - messageType: 'documentMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } else if (message.buttonMessage) { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - ...message.buttonMessage, - buttons: message.buttonMessage.buttons, - footer: message.buttonMessage.footer, - body: message.buttonMessage.body, - quoted, - }, - messageType: 'buttonMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } else if (message.listMessage) { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - ...message.listMessage, - quoted, - }, - messageType: 'listMessage', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } else { - messageRaw = { - key: { fromMe: true, id: messageId, remoteJid: number }, - message: { - ...message, - quoted, - }, - messageType: 'conversation', - messageTimestamp: Math.round(new Date().getTime() / 1000), - webhookUrl, - source: 'unknown', - instanceId: this.instanceId, - }; - } - - if (messageRaw.message.contextInfo) { - messageRaw.contextInfo = { - ...messageRaw.message.contextInfo, - }; - } - - if (messageRaw.contextInfo?.stanzaId) { - const key: any = { - id: messageRaw.contextInfo.stanzaId, - }; - - const findMessage = await this.prismaRepository.message.findFirst({ - where: { - instanceId: this.instanceId, - key, - }, - }); - - if (findMessage) { - messageRaw.contextInfo.quotedMessage = findMessage.message; - } - } - - const { base64 } = messageRaw.message; - delete messageRaw.message.base64; - - if (base64 || file || audioFile) { - if (this.configService.get('S3').ENABLE) { - try { - // Verificação adicional para garantir que há conteúdo de mídia real - const hasRealMedia = this.hasValidMediaContent(messageRaw); - - if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); - } else { - const fileBuffer = audioFile?.buffer || file?.buffer; - const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer; - - let mediaType: string; - let mimetype = audioFile?.mimetype || file.mimetype; - - if (messageRaw.messageType === 'documentMessage') { - mediaType = 'document'; - mimetype = !mimetype ? 'application/pdf' : mimetype; - } else if (messageRaw.messageType === 'imageMessage') { - mediaType = 'image'; - mimetype = !mimetype ? 'image/png' : mimetype; - } else if (messageRaw.messageType === 'audioMessage') { - mediaType = 'audio'; - mimetype = !mimetype ? 'audio/mp4' : mimetype; - } else if (messageRaw.messageType === 'videoMessage') { - mediaType = 'video'; - mimetype = !mimetype ? 'video/mp4' : mimetype; - } - - const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`; - - const size = buffer.byteLength; - - const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName); - - await s3Service.uploadFile(fullName, buffer, size, { - 'Content-Type': mimetype, - }); - - const mediaUrl = await s3Service.getObjectUrl(fullName); - - messageRaw.message.mediaUrl = mediaUrl; - } - } catch (error) { - this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); - } - } - } - - this.logger.log(messageRaw); - - this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) { - this.chatwootService.eventWhatsapp( - Events.SEND_MESSAGE, - { instanceName: this.instance.name, instanceId: this.instanceId }, - messageRaw, - ); - } - - if (this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration) - await chatbotController.emit({ - instance: { instanceName: this.instance.name, instanceId: this.instanceId }, - remoteJid: messageRaw.key.remoteJid, - msg: messageRaw, - pushName: messageRaw.pushName, - }); - - await this.prismaRepository.message.create({ - data: messageRaw, - }); - - return messageRaw; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - public async textMessage(data: SendTextDto, isIntegration = false) { - const res = await this.sendMessageWithTyping( - data.number, - { - conversation: data.text, - }, - { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - linkPreview: data?.linkPreview, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }, - null, - isIntegration, - ); - return res; - } - - protected async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - } - - if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { - mediaMessage.fileName = 'image.png'; - } - - if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { - mediaMessage.fileName = 'video.mp4'; - } - - let mimetype: string | false; - - const prepareMedia: any = { - caption: mediaMessage?.caption, - fileName: mediaMessage.fileName, - mediaType: mediaMessage.mediatype, - media: mediaMessage.media, - gifPlayback: false, - }; - - if (isURL(mediaMessage.media)) { - mimetype = mimeTypes.lookup(mediaMessage.media); - } else { - mimetype = mimeTypes.lookup(mediaMessage.fileName); - } - - prepareMedia.mimetype = mimetype; - - return prepareMedia; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } - } - - public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) { - const mediaData: SendMediaDto = { ...data }; - - if (file) mediaData.media = file.buffer.toString('base64'); - - 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, - }, - file, - isIntegration, - ); - - return mediaSent; - } - - public async processAudio(audio: string, number: string, file: any) { - number = number.replace(/\D/g, ''); - const hash = `${number}-${new Date().getTime()}`; - - const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); - if (audioConverterConfig.API_URL) { - try { - this.logger.verbose('Using audio converter API'); - const formData = new FormData(); - - if (file) { - formData.append('file', file.buffer, { - filename: file.originalname, - contentType: file.mimetype, - }); - } else if (isURL(audio)) { - formData.append('url', audio); - } else { - formData.append('base64', audio); - } - - formData.append('format', 'mp4'); - - const response = await axios.post(audioConverterConfig.API_URL, formData, { - headers: { - ...formData.getHeaders(), - apikey: audioConverterConfig.API_KEY, - }, - }); - - if (!response?.data?.audio) { - throw new InternalServerErrorException('Failed to convert audio'); - } - - const prepareMedia: any = { - fileName: `${hash}.mp4`, - mediaType: 'audio', - media: response?.data?.audio, - mimetype: 'audio/mpeg', - }; - - return prepareMedia; - } catch (error) { - this.logger.error(error?.response?.data || error); - throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error); - } - } else { - let mimetype: string; - - const prepareMedia: any = { - fileName: `${hash}.mp3`, - mediaType: 'audio', - media: audio, - mimetype: 'audio/mpeg', - }; - - if (isURL(audio)) { - mimetype = mimeTypes.lookup(audio).toString(); - } else { - mimetype = mimeTypes.lookup(prepareMedia.fileName).toString(); - } - - prepareMedia.mimetype = mimetype; - - return prepareMedia; - } - } - - public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { - 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.'); - } - - const message = await this.processAudio(mediaData.audio, data.number, file); - - 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, - }, - file, - isIntegration, - ); - - return audioSent; - } - - public async buttonMessage(data: SendButtonsDto, isIntegration = false) { - return await this.sendMessageWithTyping( - data.number, - { - buttonMessage: { - title: data.title, - description: data.description, - footer: data.footer, - buttons: data.buttons, - }, - }, - { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }, - null, - isIntegration, - ); - } - public async locationMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async listMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async templateMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async contactMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async reactionMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async getBase64FromMediaMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async deleteMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async mediaSticker() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async pollMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async statusMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async reloadConnection() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async whatsappNumber() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async markMessageAsRead() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async archiveChat() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async markChatUnread() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fetchProfile() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async offerCall() { - throw new BadRequestException('Method not available on WhatsApp Business API'); - } - public async sendPresence() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async setPresence() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fetchPrivacySettings() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updatePrivacySettings() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fetchBusinessProfile() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateProfileName() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateProfileStatus() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateProfilePicture() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async removeProfilePicture() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async blockUser() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateMessage() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async createGroup() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateGroupPicture() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateGroupSubject() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateGroupDescription() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async findGroup() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fetchAllGroups() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async inviteCode() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async inviteInfo() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async sendInvite() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async acceptInviteCode() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async revokeInviteCode() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async findParticipants() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateGParticipant() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async updateGSetting() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async toggleEphemeral() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async leaveGroup() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fetchLabels() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async handleLabel() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async receiveMobileCode() { - throw new BadRequestException('Method not available on Evolution Channel'); - } - public async fakeCall() { - throw new BadRequestException('Method not available on Evolution Channel'); - } -} diff --git a/src/api/integrations/channel/evolution/evolution.controller.ts b/src/api/integrations/channel/evolution/evolution.controller.ts deleted file mode 100644 index c9f36585..00000000 --- a/src/api/integrations/channel/evolution/evolution.controller.ts +++ /dev/null @@ -1,39 +0,0 @@ -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); - } - - integrationEnabled: boolean; - - public async receiveWebhook(data: any) { - const numberId = data.numberId; - - if (!numberId) { - this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found'); - return; - } - - const instance = await this.prismaRepository.instance.findFirst({ - where: { number: numberId }, - }); - - if (!instance) { - this.logger.error('WebhookService -> receiveWebhook -> instance not found'); - return; - } - - 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 deleted file mode 100644 index 1ab0ec00..00000000 --- a/src/api/integrations/channel/evolution/evolution.router.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RouterBroker } from '@api/abstract/abstract.router'; -import { evolutionController } from '@api/server.module'; -import { ConfigService } from '@config/env.config'; -import { Router } from 'express'; - -export class EvolutionRouter extends RouterBroker { - 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); - }); - } - - public readonly router: Router = Router(); -} diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 385fe17b..35ffa11e 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -14,7 +14,6 @@ import { SendMessageController } from './controllers/sendMessage.controller'; import { SettingsController } from './controllers/settings.controller'; import { TemplateController } from './controllers/template.controller'; import { ChannelController } from './integrations/channel/channel.controller'; -import { EvolutionController } from './integrations/channel/evolution/evolution.controller'; import { MetaController } from './integrations/channel/meta/meta.controller'; import { BaileysController } from './integrations/channel/whatsapp/baileys.controller'; import { ChatbotController } from './integrations/chatbot/chatbot.controller'; @@ -112,7 +111,6 @@ export const chatbotController = new ChatbotController(prismaRepository, waMonit export const channelController = new ChannelController(prismaRepository, waMonitor); // channels -export const evolutionController = new EvolutionController(prismaRepository, waMonitor); export const metaController = new MetaController(prismaRepository, waMonitor); export const baileysController = new BaileysController(waMonitor); diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 2bb3dc1e..247f92de 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -152,5 +152,4 @@ export const MessageSubtype = [ export const Integration = { WHATSAPP_BUSINESS: 'WHATSAPP-BUSINESS', WHATSAPP_BAILEYS: 'WHATSAPP-BAILEYS', - EVOLUTION: 'EVOLUTION', };