Whatsapp Business support
This commit is contained in:
@@ -32,6 +32,9 @@ func (s *Server) setupRoutes() *http.ServeMux {
|
||||
// Serve media files (with auth)
|
||||
mux.HandleFunc("/api/media/", s.handleServeMedia)
|
||||
|
||||
// Business API webhooks (no auth - Meta validates via verify_token)
|
||||
mux.HandleFunc("/webhooks/whatsapp/", s.handleBusinessAPIWebhook)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
|
||||
151
cmd/server/routes_businessapi.go
Normal file
151
cmd/server/routes_businessapi.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/logging"
|
||||
"git.warky.dev/wdevs/whatshooked/internal/whatsapp/businessapi"
|
||||
)
|
||||
|
||||
// handleBusinessAPIWebhook handles both verification (GET) and webhook events (POST)
|
||||
func (s *Server) handleBusinessAPIWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
s.handleBusinessAPIWebhookVerify(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
s.handleBusinessAPIWebhookEvent(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// handleBusinessAPIWebhookVerify handles webhook verification from Meta
|
||||
// GET /webhooks/whatsapp/{accountID}?hub.mode=subscribe&hub.verify_token=XXX&hub.challenge=YYY
|
||||
func (s *Server) handleBusinessAPIWebhookVerify(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 s.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)
|
||||
w.Write([]byte(challenge))
|
||||
return
|
||||
}
|
||||
|
||||
logging.Warn("Webhook verification failed",
|
||||
"account_id", accountID,
|
||||
"mode", mode,
|
||||
"token_match", token == accountConfig.VerifyToken)
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
|
||||
// handleBusinessAPIWebhookEvent handles incoming webhook events from Meta
|
||||
// POST /webhooks/whatsapp/{accountID}
|
||||
func (s *Server) handleBusinessAPIWebhookEvent(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 := s.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)
|
||||
w.Write([]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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user