package handlers import ( "net/http" "strings" "git.warky.dev/wdevs/whatshooked/pkg/logging" "git.warky.dev/wdevs/whatshooked/pkg/whatsapp/businessapi" ) // BusinessAPIWebhook handles both verification (GET) and webhook events (POST) func (h *Handlers) BusinessAPIWebhook(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { h.businessAPIWebhookVerify(w, r) return } if r.Method == http.MethodPost { h.businessAPIWebhookEvent(w, r) return } http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } // businessAPIWebhookVerify handles webhook verification from Meta // GET /webhooks/whatsapp/{accountID}?hub.mode=subscribe&hub.verify_token=XXX&hub.challenge=YYY func (h *Handlers) businessAPIWebhookVerify(w http.ResponseWriter, r *http.Request) { // Extract account ID from URL path accountID := extractAccountIDFromPath(r.URL.Path) if accountID == "" { http.Error(w, "Account ID required in path", http.StatusBadRequest) return } // Get the account configuration var accountConfig *struct { ID string Type string VerifyToken string } for _, cfg := range h.config.WhatsApp { if cfg.ID == accountID && cfg.Type == "business-api" { if cfg.BusinessAPI != nil { accountConfig = &struct { ID string Type string VerifyToken string }{ ID: cfg.ID, Type: cfg.Type, VerifyToken: cfg.BusinessAPI.VerifyToken, } break } } } if accountConfig == nil { logging.Error("Business API account not found or not configured", "account_id", accountID) http.Error(w, "Account not found", http.StatusNotFound) return } // Get query parameters mode := r.URL.Query().Get("hub.mode") token := r.URL.Query().Get("hub.verify_token") challenge := r.URL.Query().Get("hub.challenge") logging.Info("Webhook verification request", "account_id", accountID, "mode", mode, "has_challenge", challenge != "") // Verify the token matches if mode == "subscribe" && token == accountConfig.VerifyToken { logging.Info("Webhook verification successful", "account_id", accountID) w.WriteHeader(http.StatusOK) writeBytes(w, []byte(challenge)) return } logging.Warn("Webhook verification failed", "account_id", accountID, "mode", mode, "token_match", token == accountConfig.VerifyToken) http.Error(w, "Forbidden", http.StatusForbidden) } // businessAPIWebhookEvent handles incoming webhook events from Meta // POST /webhooks/whatsapp/{accountID} func (h *Handlers) businessAPIWebhookEvent(w http.ResponseWriter, r *http.Request) { // Extract account ID from URL path accountID := extractAccountIDFromPath(r.URL.Path) if accountID == "" { http.Error(w, "Account ID required in path", http.StatusBadRequest) return } // Get the client from the manager client, exists := h.whatsappMgr.GetClient(accountID) if !exists { logging.Error("Client not found for webhook", "account_id", accountID) http.Error(w, "Account not found", http.StatusNotFound) return } // Verify it's a Business API client if client.GetType() != "business-api" { logging.Error("Account is not a Business API client", "account_id", accountID, "type", client.GetType()) http.Error(w, "Not a Business API account", http.StatusBadRequest) return } // Cast to Business API client to access HandleWebhook baClient, ok := client.(*businessapi.Client) if !ok { logging.Error("Failed to cast to Business API client", "account_id", accountID) http.Error(w, "Internal error", http.StatusInternalServerError) return } // Process the webhook if err := baClient.HandleWebhook(r); err != nil { logging.Error("Failed to process webhook", "account_id", accountID, "error", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } // Return 200 OK to acknowledge receipt w.WriteHeader(http.StatusOK) writeBytes(w, []byte("OK")) } // extractAccountIDFromPath extracts the account ID from the URL path // Example: /webhooks/whatsapp/business -> "business" func extractAccountIDFromPath(path string) string { // Remove trailing slash if present path = strings.TrimSuffix(path, "/") // Split by / parts := strings.Split(path, "/") // Expected format: /webhooks/whatsapp/{accountID} if len(parts) >= 4 { return parts[3] } return "" }