--- description: Channel integration patterns for Evolution API globs: - "src/api/integrations/channel/**/*.ts" alwaysApply: false --- # Evolution API Channel Integration Rules ## Channel Controller Pattern ### Base Channel Controller ```typescript export interface ChannelControllerInterface { integrationEnabled: boolean; } export class ChannelController { public prismaRepository: PrismaRepository; public waMonitor: WAMonitoringService; constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { this.prisma = prismaRepository; this.monitor = waMonitor; } public set prisma(prisma: PrismaRepository) { this.prismaRepository = prisma; } public get prisma() { return this.prismaRepository; } public set monitor(waMonitor: WAMonitoringService) { this.waMonitor = waMonitor; } public get monitor() { return this.waMonitor; } public init(instanceData: InstanceDto, data: ChannelDataType) { if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) { throw new BadRequestException('token is required'); } if (instanceData.integration === Integration.WHATSAPP_BUSINESS) { return new BusinessStartupService(/* dependencies */); } if (instanceData.integration === Integration.EVOLUTION) { return new EvolutionStartupService(/* dependencies */); } if (instanceData.integration === Integration.WHATSAPP_BAILEYS) { return new BaileysStartupService(/* dependencies */); } return null; } } ``` ### 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 ```typescript export class ChannelStartupService { constructor( private readonly configService: ConfigService, private readonly eventEmitter: EventEmitter2, private readonly prismaRepository: PrismaRepository, public readonly cache: CacheService, public readonly chatwootCache: CacheService, ) {} public readonly logger = new Logger('ChannelStartupService'); public client: WASocket; public readonly instance: wa.Instance = {}; public readonly localChatwoot: wa.LocalChatwoot = {}; public readonly localProxy: wa.LocalProxy = {}; public readonly localSettings: wa.LocalSettings = {}; public readonly localWebhook: wa.LocalWebHook = {}; public setInstance(instance: InstanceDto) { this.logger.setInstance(instance.instanceName); 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 }, { instance: this.instance.name, status: 'created', }, ); } } public set instanceName(name: string) { this.logger.setInstance(name); this.instance.name = name; } public get instanceName() { return this.instance.name; } } ``` ### 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 ```typescript export class BusinessStartupService extends ChannelStartupService { constructor( configService: ConfigService, eventEmitter: EventEmitter2, prismaRepository: PrismaRepository, cache: CacheService, chatwootCache: CacheService, baileysCache: CacheService, providerFiles: ProviderFiles, ) { super(configService, eventEmitter, prismaRepository, cache, chatwootCache); } public async sendMessage(data: SendTextDto): Promise { const businessConfig = this.configService.get('WA_BUSINESS'); const payload = { messaging_product: 'whatsapp', to: data.number, type: 'text', text: { body: data.text, }, }; try { const response = await axios.post( `${businessConfig.URL}/${businessConfig.VERSION}/${this.instance.businessId}/messages`, payload, { headers: { 'Authorization': `Bearer ${this.instance.token}`, 'Content-Type': 'application/json', }, } ); return response.data; } catch (error) { this.logger.error(`Business API call failed: ${error.message}`); throw new BadRequestException('Failed to send message via Business API'); } } public async receiveWebhook(data: any): Promise { // Process incoming webhook from Meta Business API const { entry } = data; for (const entryItem of entry) { const { changes } = entryItem; for (const change of changes) { if (change.field === 'messages') { await this.processMessage(change.value); } } } } private async processMessage(messageData: any): Promise { // Process incoming message from Business API const { messages, contacts } = messageData; if (messages) { for (const message of messages) { await this.handleIncomingMessage(message, contacts); } } } private async handleIncomingMessage(message: any, contacts: any[]): Promise { // Handle individual message const contact = contacts?.find(c => c.wa_id === message.from); // Emit event for message processing this.eventEmitter.emit(Events.MESSAGES_UPSERT, { instanceName: this.instance.name, message, contact, }); } } ``` ## Baileys Service Pattern ### Baileys Integration Service ```typescript export class BaileysStartupService extends ChannelStartupService { constructor( configService: ConfigService, eventEmitter: EventEmitter2, prismaRepository: PrismaRepository, cache: CacheService, chatwootCache: CacheService, baileysCache: CacheService, providerFiles: ProviderFiles, ) { super(configService, eventEmitter, prismaRepository, cache, chatwootCache); } public async connectToWhatsapp(): Promise { const authPath = path.join(INSTANCE_DIR, this.instance.name); const { state, saveCreds } = await useMultiFileAuthState(authPath); this.client = makeWASocket({ auth: state, logger: P({ level: 'error' }), printQRInTerminal: false, browser: ['Evolution API', 'Chrome', '4.0.0'], defaultQueryTimeoutMs: 60000, }); this.setupEventHandlers(); this.client.ev.on('creds.update', saveCreds); } private setupEventHandlers(): void { this.client.ev.on('connection.update', (update) => { this.handleConnectionUpdate(update); }); this.client.ev.on('messages.upsert', ({ messages, type }) => { this.handleIncomingMessages(messages, type); }); this.client.ev.on('messages.update', (updates) => { this.handleMessageUpdates(updates); }); this.client.ev.on('contacts.upsert', (contacts) => { this.handleContactsUpdate(contacts); }); this.client.ev.on('chats.upsert', (chats) => { this.handleChatsUpdate(chats); }); } private async handleConnectionUpdate(update: ConnectionUpdate): Promise { const { connection, lastDisconnect, qr } = update; if (qr) { this.instance.qrcode = { count: this.instance.qrcode?.count ? this.instance.qrcode.count + 1 : 1, base64: qr, }; this.eventEmitter.emit(Events.QRCODE_UPDATED, { instanceName: this.instance.name, qrcode: this.instance.qrcode, }); } if (connection === 'close') { const shouldReconnect = (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; if (shouldReconnect) { this.logger.log('Connection closed, reconnecting...'); await this.connectToWhatsapp(); } else { this.logger.log('Connection closed, logged out'); this.eventEmitter.emit(Events.LOGOUT_INSTANCE, { instanceName: this.instance.name, }); } } if (connection === 'open') { this.logger.log('Connection opened successfully'); this.instance.wuid = this.client.user?.id; this.eventEmitter.emit(Events.CONNECTION_UPDATE, { instanceName: this.instance.name, state: 'open', }); } } public async sendMessage(data: SendTextDto): Promise { const jid = createJid(data.number); const message = { text: data.text, }; if (data.linkPreview !== undefined) { message.linkPreview = data.linkPreview; } if (data.mentionsEveryOne) { // Handle mentions } try { const response = await this.client.sendMessage(jid, message); return response; } catch (error) { this.logger.error(`Failed to send message: ${error.message}`); throw new BadRequestException('Failed to send message'); } } } ``` ## Channel Router Pattern ### Channel Router Structure ```typescript export class ChannelRouter { public readonly router: Router; 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 ```typescript type ChannelDataType = { configService: ConfigService; eventEmitter: EventEmitter2; prismaRepository: PrismaRepository; cache: CacheService; chatwootCache: CacheService; baileysCache: CacheService; providerFiles: ProviderFiles; }; export enum Integration { WHATSAPP_BUSINESS = 'WHATSAPP-BUSINESS', WHATSAPP_BAILEYS = 'WHATSAPP-BAILEYS', EVOLUTION = 'EVOLUTION', } ``` ## Error Handling in Channels ### Channel-Specific Error Handling ```typescript // CORRECT - Channel-specific error handling public async sendMessage(data: SendTextDto): Promise { try { const response = await this.channelSpecificSend(data); return response; } catch (error) { this.logger.error(`${this.constructor.name} send failed: ${error.message}`); if (error.response?.status === 401) { throw new UnauthorizedException('Invalid token for channel'); } if (error.response?.status === 429) { throw new BadRequestException('Rate limit exceeded'); } throw new InternalServerErrorException('Channel communication failed'); } } ``` ## 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'), }), }) ); }); }); }); ```