import { Injectable, Logger } from '@nestjs/common'; import axios, { AxiosResponse } from 'axios'; import { IntegrationStrategy, IntegrationResult, } from '../integration.strategy'; interface WhatsAppConfig { accessToken: string; phoneNumberId: string; apiVersion?: string; businessAccountId?: string; } interface WhatsAppMessage { messaging_product: string; to: string; type: string; text?: { body: string; preview_url?: boolean; }; template?: { name: string; language: { code: string; }; components?: any[]; }; image?: { link?: string; caption?: string; }; document?: { link?: string; caption?: string; filename?: string; }; audio?: { link: string; }; video?: { link?: string; caption?: string; }; location?: { latitude: number; longitude: number; name?: string; address?: string; }; interactive?: { type: string; body?: { text: string; }; footer?: { text: string; }; action: any; }; } interface WhatsAppApiResponse { messaging_product: string; contacts: Array<{ input: string; wa_id: string; }>; messages: Array<{ id: string; message_status?: string; }>; } @Injectable() export class WhatsAppCloudStrategy implements IntegrationStrategy { private readonly logger = new Logger(WhatsAppCloudStrategy.name); private readonly baseUrl = 'https://graph.facebook.com'; async sendMessage( to: string, message: string, config: any, ): Promise { try { if (!this.validateConfig(config)) { throw new Error('Invalid WhatsApp Cloud API configuration'); } const whatsappConfig = config as WhatsAppConfig; const apiVersion = whatsappConfig.apiVersion || 'v18.0'; const url = `${this.baseUrl}/${apiVersion}/${whatsappConfig.phoneNumberId}/messages`; // Determine message type and prepare payload const payload = this.prepareMessagePayload(to, message, config); const response: AxiosResponse = await axios.post( url, payload, { headers: { Authorization: `Bearer ${whatsappConfig.accessToken}`, 'Content-Type': 'application/json', }, timeout: 30000, }, ); if (response.data?.messages?.[0]?.id) { this.logger.log( `WhatsApp message sent successfully to ${to}, messageId: ${response.data.messages[0].id}`, ); return { success: true, messageId: response.data.messages[0].id, provider: 'whatsapp-cloud', service: 'API', timestamp: new Date(), }; } else { throw new Error('Invalid response from WhatsApp API'); } } catch (error) { this.logger.error( `Failed to send WhatsApp message to ${to}`, error.response?.data || error.message, ); return { success: false, provider: 'whatsapp-cloud', service: 'API', error: this.extractErrorMessage(error), timestamp: new Date(), }; } } private prepareMessagePayload( to: string, message: string, config: any, ): WhatsAppMessage { const basePayload: WhatsAppMessage = { messaging_product: 'whatsapp', to: this.formatPhoneNumber(to), type: 'text', }; // Handle different message types based on config if (config.messageType) { switch (config.messageType) { case 'template': return { ...basePayload, type: 'template', template: { name: config.templateName || 'hello_world', language: { code: config.languageCode || 'en_US', }, components: config.templateComponents || [], }, }; case 'image': return { ...basePayload, type: 'image', image: { link: config.mediaUrl, caption: message || config.caption, }, }; case 'document': return { ...basePayload, type: 'document', document: { link: config.mediaUrl, caption: message || config.caption, filename: config.filename, }, }; case 'audio': return { ...basePayload, type: 'audio', audio: { link: config.mediaUrl, }, }; case 'video': return { ...basePayload, type: 'video', video: { link: config.mediaUrl, caption: message || config.caption, }, }; case 'location': return { ...basePayload, type: 'location', location: { latitude: config.latitude, longitude: config.longitude, name: config.locationName, address: config.locationAddress, }, }; case 'interactive': return { ...basePayload, type: 'interactive', interactive: { type: config.interactiveType || 'button', body: { text: message, }, footer: config.footer ? { text: config.footer } : undefined, action: config.interactiveAction, }, }; default: // Fall back to text message break; } } // Default text message return { ...basePayload, text: { body: message, preview_url: config.previewUrl !== false, // Default true }, }; } private formatPhoneNumber(phoneNumber: string): string { // Remove any non-digit characters except + let formatted = phoneNumber.replace(/[^\d+]/g, ''); // If it doesn't start with +, add country code if provided if (!formatted.startsWith('+')) { formatted = `+${formatted}`; } // Remove + for WhatsApp API (it expects numbers without +) return formatted.substring(1); } private extractErrorMessage(error: any): string { if (error.response?.data?.error?.message) { return error.response.data.error.message; } if (error.response?.data?.error?.error_user_msg) { return error.response.data.error.error_user_msg; } if (error.response?.data?.message) { return error.response.data.message; } if (error.message) { return error.message; } return 'Unknown WhatsApp API error'; } validateConfig(config: any): boolean { if (!config || typeof config !== 'object') { return false; } // Required fields if (!config.accessToken || !config.phoneNumberId) { return false; } // Validate message type specific requirements if (config.messageType) { switch (config.messageType) { case 'template': if (!config.templateName) { return false; } break; case 'image': case 'document': case 'audio': case 'video': if (!config.mediaUrl) { return false; } break; case 'location': if ( typeof config.latitude !== 'number' || typeof config.longitude !== 'number' ) { return false; } break; case 'interactive': if (!config.interactiveAction) { return false; } break; } } return true; } async getPhoneNumberInfo(config: WhatsAppConfig): Promise { try { const apiVersion = config.apiVersion || 'v18.0'; const url = `${this.baseUrl}/${apiVersion}/${config.phoneNumberId}`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${config.accessToken}`, }, }); return response.data; } catch (error) { this.logger.error( 'Failed to get phone number info', error.response?.data || error.message, ); throw new Error( `Failed to get phone number info: ${this.extractErrorMessage(error)}`, ); } } async getBusinessProfile(config: WhatsAppConfig): Promise { try { const apiVersion = config.apiVersion || 'v18.0'; const url = `${this.baseUrl}/${apiVersion}/${config.phoneNumberId}/whatsapp_business_profile`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${config.accessToken}`, }, }); return response.data; } catch (error) { this.logger.error( 'Failed to get business profile', error.response?.data || error.message, ); throw new Error( `Failed to get business profile: ${this.extractErrorMessage(error)}`, ); } } async updateBusinessProfile( config: WhatsAppConfig, profileData: any, ): Promise { try { const apiVersion = config.apiVersion || 'v18.0'; const url = `${this.baseUrl}/${apiVersion}/${config.phoneNumberId}/whatsapp_business_profile`; const response = await axios.post(url, profileData, { headers: { Authorization: `Bearer ${config.accessToken}`, 'Content-Type': 'application/json', }, }); return response.data; } catch (error) { this.logger.error( 'Failed to update business profile', error.response?.data || error.message, ); throw new Error( `Failed to update business profile: ${this.extractErrorMessage(error)}`, ); } } }