package services import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) var umindWhatsappHTTPClient = &http.Client{Timeout: 20 * time.Second} const whatsappGraphAPIVersion = "v21.0" // ValidarFirmaWhatsApp valida X-Hub-Signature-256 — es la única autenticación // real del webhook de WhatsApp (a diferencia del widget, que solo valida // Origin/Referer). Meta firma el body crudo con HMAC-SHA256 usando el App // Secret; sin validar esto, cualquiera que adivine la URL del webhook podría // mandar mensajes falsos a nombre de un visitante. func ValidarFirmaWhatsApp(appSecret string, body []byte, signatureHeader string) bool { const prefix = "sha256=" if !strings.HasPrefix(signatureHeader, prefix) { return false } esperada, err := hex.DecodeString(strings.TrimPrefix(signatureHeader, prefix)) if err != nil { return false } mac := hmac.New(sha256.New, []byte(appSecret)) mac.Write(body) return hmac.Equal(mac.Sum(nil), esperada) } // ProcesarMensajeWhatsAppUmind adapta un mensaje entrante de WhatsApp Business // Cloud API al mismo motor que atiende el widget web y Telegram. func ProcesarMensajeWhatsAppUmind(canal *models.UmindCanal, from, texto string) error { tenant, err := models.GetUmindTenantByID(canal.TenantID) if err != nil || !tenant.Activo { return fmt.Errorf("tenant no encontrado o inactivo: %w", err) } credenciales, err := DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return fmt.Errorf("credenciales del canal corruptas: %w", err) } phoneNumberID := credenciales["phone_number_id"] accessToken := credenciales["access_token"] if phoneNumberID == "" || accessToken == "" { return fmt.Errorf("el canal no tiene phone_number_id/access_token configurados") } sessionID := fmt.Sprintf("wa:%s", from) respuesta, err := ProcessWidgetMessage(tenant, sessionID, texto) if err != nil { return fmt.Errorf("error del agente: %w", err) } return enviarMensajeWhatsApp(phoneNumberID, accessToken, from, respuesta) } func enviarMensajeWhatsApp(phoneNumberID, accessToken, to, texto string) error { payload := map[string]interface{}{ "messaging_product": "whatsapp", "to": to, "type": "text", "text": map[string]string{"body": texto}, } body, err := json.Marshal(payload) if err != nil { return err } url := fmt.Sprintf("https://graph.facebook.com/%s/%s/messages", whatsappGraphAPIVersion, phoneNumberID) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := umindWhatsappHTTPClient.Do(req) if err != nil { return fmt.Errorf("no se pudo contactar la API de WhatsApp: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("WhatsApp respondió %d", resp.StatusCode) } return nil }