package webhook // TikTok Gin webhook adapter — bridges HTTP requests from the Gin router // to the TikTok channel package's WebhookHandler and IncomingProcessor. // Reference: Facebook webhook adapter pattern (facebook_webhook.go) import ( "encoding/json" "io" "net/http" "strconv" tiktokchannel "github.com/gochat/gochat/internal/channel/tiktok" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // TikTokWebhookHandler adapts TikTok webhook handling to Gin HTTP requests. type TikTokWebhookHandler struct { tiktokWebhook *tiktokchannel.WebhookHandler pipeline *tiktokchannel.IncomingProcessor db *gorm.DB } // NewTikTokWebhookHandler creates a Gin-compatible TikTok webhook handler. func NewTikTokWebhookHandler(tiktokWebhook *tiktokchannel.WebhookHandler, pipeline *tiktokchannel.IncomingProcessor, db *gorm.DB) *TikTokWebhookHandler { return &TikTokWebhookHandler{ tiktokWebhook: tiktokWebhook, pipeline: pipeline, db: db, } } // HandleTikTokWebhook processes incoming TikTok webhook HTTP requests. func (h *TikTokWebhookHandler) HandleTikTokWebhook(c *gin.Context) { inboxIDStr := c.Param("inbox_id") inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32) if err != nil { applogger.L().Warnf("TikTok webhook: invalid inbox_id %s", inboxIDStr) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Lookup inbox from database inbox, err := h.lookupInbox(uint(inboxID)) if err != nil { applogger.L().Warnf("TikTok webhook: inbox lookup failed for id %d: %v", inboxID, err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Parse the webhook event using the channel-level handler event, err := h.tiktokWebhook.HandleWebhookRequest(c.Request) if err != nil { applogger.L().Errorf("TikTok webhook: parse request failed for inbox %d: %v", inboxID, err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Process the event via the pipeline if err := h.pipeline.ProcessUpdate(c.Request.Context(), inbox, *event); err != nil { applogger.L().Errorf("TikTok webhook: process event failed for inbox %d: %v", inboxID, err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } c.JSON(http.StatusOK, gin.H{"status": "processed"}) } // HandleTikTokVerification handles TikTok webhook verification (challenge-response). func (h *TikTokWebhookHandler) HandleTikTokVerification(c *gin.Context) { inboxIDStr := c.Param("inbox_id") inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox_id"}) return } body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to read body"}) return } defer c.Request.Body.Close() // Parse verification challenge var verifyReq map[string]interface{} if err := json.Unmarshal(body, &verifyReq); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) return } challenge, _ := verifyReq["challenge"].(string) applogger.L().Infof("TikTok webhook verification: inbox=%d challenge=%s", inboxID, challenge) c.JSON(http.StatusOK, gin.H{"challenge": challenge}) } // lookupInbox fetches an Inbox record from the database. func (h *TikTokWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) { var inbox model.Inbox if err := h.db.Where("id = ?", inboxID).First(&inbox).Error; err != nil { return nil, err } return &inbox, nil } // parseChannelConfig parses the JSON-encoded ChannelConfig string into a map. func (h *TikTokWebhookHandler) parseChannelConfig(inbox *model.Inbox) map[string]interface{} { if inbox.ChannelConfig == "" { return map[string]interface{}{} } var config map[string]interface{} if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil { applogger.L().Warnf("TikTok: Failed to parse ChannelConfig for inbox %d: %v", inbox.ID, err) return map[string]interface{}{} } return config }