package channel import ( "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" ) // WebhookHandler provides unified webhook callback entry point for all channels. // Reference: P2D §6 — all external channel callbacks route through /webhooks/{channel_type}/{identifier} // Corresponds to Chatwoot's per-channel WebhooksController (Telegram, Facebook, WhatsApp etc.) // // Updated to use MessageBroker + Dispatcher instead of calling ChannelProvider directly. // The flow is now: webhook → WebhookHandler → MessageBroker.HandleIncoming → // IncomingMessageProcessor pipeline → Dispatcher.Dispatch (events to listeners) type WebhookHandler struct { inboxRepo InboxRepository broker Broker dispatcher *Dispatcher } // InboxRepository abstracts inbox lookup for webhook processing. type InboxRepository interface { FindByChannelTypeAndIdentifier(channelType string, identifier string) (*model.Inbox, error) FindByID(id uint) (*model.Inbox, error) } // NewWebhookHandler creates a new webhook handler with the given dependencies. // broker routes the message through the appropriate ChannelProvider's pipeline. // dispatcher broadcasts events (message.created, conversation.created, etc.) to registered listeners. func NewWebhookHandler(inboxRepo InboxRepository, broker Broker, dispatcher *Dispatcher) *WebhookHandler { return &WebhookHandler{ inboxRepo: inboxRepo, broker: broker, dispatcher: dispatcher, } } // RegisterRoutes registers webhook routes on the given router group. // Pattern: /webhooks/{channel_type}/{identifier} // Example: /webhooks/telegram/123456:ABC-DEF (bot_token as identifier) // Example: /webhooks/facebook/123456789 (page_id as identifier) // The router group may have middleware applied (e.g. WebhookAuth for token-based verification). func (h *WebhookHandler) RegisterRoutes(g *gin.RouterGroup) { g.POST("/:channel_type/:identifier", h.HandleWebhook) g.GET("/:channel_type/:identifier", h.HandleWebhookVerification) // for FB/WhatsApp verification } // HandleWebhook is the unified POST webhook callback handler. // Flow: // 1. Parse channel_type + identifier from URL params // 2. Look up the Inbox via InboxRepository // 3. Extract raw request body + headers + query params // 4. Find the ChannelProvider via registry and validate webhook authenticity // 5. Parse the raw body into an IncomingMessage via the provider // 6. Route through MessageBroker.HandleIncoming (pipeline: validate → resolve → persist) // 7. Dispatch events via Dispatcher (webhook, notification, status listeners) func (h *WebhookHandler) HandleWebhook(c *gin.Context) { channelType := ChannelType(c.Param("channel_type")) identifier := c.Param("identifier") // Check if channel provider is registered provider, err := Get(channelType) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported channel type"}) return } // Find inbox by channel type and identifier inbox, err := h.inboxRepo.FindByChannelTypeAndIdentifier(string(channelType), identifier) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "inbox not found"}) return } // Build WebhookRequest for provider validation body, _ := c.GetRawData() headers := make(map[string]string) for k, v := range c.Request.Header { if len(v) > 0 { headers[k] = v[0] } } queryParams := make(map[string]string) for k, v := range c.Request.URL.Query() { if len(v) > 0 { queryParams[k] = v[0] } } webhookReq := &WebhookRequest{ ChannelType: channelType, Identifier: identifier, Headers: headers, Body: body, QueryParams: queryParams, Method: c.Request.Method, } // Validate webhook request authenticity (signature verification) if err := provider.ValidateWebhookRequest(c.Request.Context(), inbox, webhookReq); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "webhook validation failed"}) return } // Parse the raw webhook body into an IncomingMessage via the provider incomingMsg, err := provider.ProcessIncoming(c.Request.Context(), inbox, body) if err != nil { applogger.L().Errorf("provider.ProcessIncoming failed: channel=%s inbox=%d err=%v", channelType, inbox.ID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse incoming message"}) return } // Acknowledge to external channel immediately (before pipeline processing) // This prevents webhook timeouts for channels that require fast response (e.g. Telegram) c.JSON(http.StatusOK, gin.H{ "status": "received", "channel_type": channelType, "inbox_id": inbox.ID, "account_id": inbox.AccountID, }) // Route through MessageBroker → IncomingMessageProcessor pipeline // Pipeline stages: ValidateConfig → ResolveContact → ResolveConversation → PersistMessage → PublishEvent if err := h.broker.HandleIncoming(c.Request.Context(), inbox, incomingMsg); err != nil { applogger.L().Errorf("broker.HandleIncoming failed: channel=%s inbox=%d err=%v", channelType, inbox.ID, err) // Already responded 200 to the channel — log the error for investigation return } // Dispatch events to all registered listeners // Listeners: WebhookListener (forward to external integrations), // NotificationListener (push to agents), ChannelStatusListener (track channel health) event := &ChannelEvent{ Type: EventMessageCreated, Channel: channelType, InboxID: inbox.ID, AccountID: inbox.AccountID, Timestamp: incomingMsg.ReceivedAt.Unix(), Data: map[string]interface{}{ "inbox": inbox, "incoming_msg": incomingMsg, "channel_type": string(channelType), "source_id": incomingMsg.SourceID, }, } if err := h.dispatcher.Dispatch(c.Request.Context(), event); err != nil { applogger.L().Errorf("dispatcher.Dispatch failed: event=%s inbox=%d err=%v", event.Type, inbox.ID, err) } } // HandleWebhookVerification handles GET requests for webhook verification. // Used by Facebook/WhatsApp webhook verification (challenge-response). // This method does NOT use the broker/dispatcher — verification is a direct provider interaction. func (h *WebhookHandler) HandleWebhookVerification(c *gin.Context) { channelType := ChannelType(c.Param("channel_type")) identifier := c.Param("identifier") provider, err := Get(channelType) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported channel type"}) return } // Some channels (Facebook, WhatsApp) require GET verification // The provider handles this via ValidateWebhookRequest for GET method inbox, err := h.inboxRepo.FindByChannelTypeAndIdentifier(string(channelType), identifier) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "inbox not found"}) return } headers := make(map[string]string) for k, v := range c.Request.Header { if len(v) > 0 { headers[k] = v[0] } } queryParams := make(map[string]string) for k, v := range c.Request.URL.Query() { if len(v) > 0 { queryParams[k] = v[0] } } webhookReq := &WebhookRequest{ ChannelType: channelType, Identifier: identifier, Headers: headers, Body: nil, // GET requests have no body QueryParams: queryParams, Method: c.Request.Method, } // Provider handles verification challenge (e.g. Facebook hub.mode=subscribe, hub.challenge) if err := provider.ValidateWebhookRequest(c.Request.Context(), inbox, webhookReq); err != nil { c.JSON(http.StatusForbidden, gin.H{"error": "verification failed"}) return } // If ValidateWebhookRequest succeeded, the provider may have set a challenge response // in the webhookReq. Return 200 OK to confirm verification. c.JSON(http.StatusOK, gin.H{"status": "verified", "channel_type": channelType}) }