207 lines
7.7 KiB
Go
207 lines
7.7 KiB
Go
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})
|
|
} |