* Implement MessageCache to store events when no webhooks are available. * Add configuration options for enabling cache, setting data path, max age, and max events. * Create API endpoints for managing cached events, including listing, replaying, and deleting. * Integrate caching into the hooks manager to store events when no active webhooks are found. * Enhance logging for better traceability of cached events and operations.
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
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) {
|
|
accountID := extractAccountIDFromPath(r.URL.Path)
|
|
|
|
if r.Method == http.MethodGet {
|
|
logging.Info("WhatsApp webhook verification request", "account_id", accountID, "method", "GET")
|
|
h.businessAPIWebhookVerify(w, r)
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
logging.Info("WhatsApp webhook event received", "account_id", accountID, "method", "POST")
|
|
h.businessAPIWebhookEvent(w, r)
|
|
return
|
|
}
|
|
|
|
logging.Warn("WhatsApp webhook invalid method", "account_id", accountID, "method", r.Method)
|
|
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 == "" {
|
|
logging.Warn("WhatsApp webhook verification missing account ID")
|
|
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 == "" {
|
|
logging.Warn("WhatsApp webhook event missing account ID")
|
|
http.Error(w, "Account ID required in path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
logging.Info("WhatsApp webhook processing started", "account_id", accountID)
|
|
|
|
// 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
|
|
}
|
|
|
|
logging.Info("WhatsApp webhook processed successfully", "account_id", accountID)
|
|
|
|
// 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 ""
|
|
}
|