package v1 // TikTokChannelHandler handles TikTok Business channel-specific configuration CRUD. // Reference: This is a GoChat addition — Chatwoot does not have TikTok channel support. // // TikTok Business API: https://business-api.tiktok.com/portal/docs // // gochat maps TikTok channel operations to: // - POST /api/v1/accounts/:id/channels/tiktok_channel → create TikTok inbox // - GET /api/v1/accounts/:id/channels/tiktok_channel/:tt_id → get TikTok channel // - PATCH /api/v1/accounts/:id/channels/tiktok_channel/:tt_id → update TikTok channel // - DELETE /api/v1/accounts/:id/channels/tiktok_channel/:tt_id → delete TikTok channel // - GET /api/v1/accounts/:id/channels/tiktok_channel → list TikTok channels import ( "net/http" "strconv" "github.com/gin-gonic/gin" tiktokchannel "github.com/gochat/gochat/internal/channel/tiktok" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" ) // TikTokChannelHandler handles TikTok Business channel management. // Follows FacebookChannelHandler pattern: uses top-level ChannelTikTokService for CRUD, // internal TikTokProvider for webhook setup via OnCreate. type TikTokChannelHandler struct { ttChannelSvc *service.ChannelTikTokService ttProvider *tiktokchannel.TikTokProvider inboxSvc *service.InboxService ttRepo *repository.ChannelTikTokRepo } // NewTikTokChannelHandler creates a new TikTok channel handler. func NewTikTokChannelHandler( ttChannelSvc *service.ChannelTikTokService, ttProvider *tiktokchannel.TikTokProvider, inboxSvc *service.InboxService, ttRepo *repository.ChannelTikTokRepo, ) *TikTokChannelHandler { return &TikTokChannelHandler{ ttChannelSvc: ttChannelSvc, ttProvider: ttProvider, inboxSvc: inboxSvc, ttRepo: ttRepo, } } // === TikTok Channel CRUD === // CreateTikTokChannelRequest is the DTO for creating a TikTok Business channel inbox. type CreateTikTokChannelRequest struct { Name string `json:"name" validate:"required,min=2"` TikTokBusinessID string `json:"tiktok_business_id" validate:"required"` AccessToken string `json:"access_token" validate:"required"` InboxName string `json:"inbox_name,omitempty"` EnableAutoAssignment bool `json:"enable_auto_assignment,omitempty"` } // CreateTikTokChannel creates a new TikTok Business channel and its associated inbox. // POST /api/v1/accounts/:id/channels/tiktok_channel func (h *TikTokChannelHandler) CreateTikTokChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { applogger.L().Errorf("Invalid account_id: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } var req CreateTikTokChannelRequest if err := c.ShouldBindJSON(&req); err != nil { applogger.L().Errorf("Failed to bind create tiktok channel request: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() // 1. Create ChannelTikTok record channelRecord := &channelmodel.ChannelTikTok{ AccountID: uint(accountID), TikTokBusinessID: req.TikTokBusinessID, AccessToken: req.AccessToken, } if err := h.ttChannelSvc.Create(ctx, channelRecord); err != nil { applogger.L().Errorf("Failed to create TikTok channel: %v", err) c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create TikTok channel", "details": err.Error()}) return } // 2. Create Inbox record with channel_type=tiktok inboxName := req.InboxName if inboxName == "" { inboxName = req.Name } createdInbox, err := h.inboxSvc.Create(ctx, uint(accountID), service.CreateInboxRequest{ Name: inboxName, ChannelType: "tiktok", EnableAutoAssignment: req.EnableAutoAssignment, }) if err != nil { applogger.L().Errorf("Failed to create TikTok inbox: %v", err) c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create TikTok inbox", "details": err.Error()}) return } // 3. Link ChannelTikTok.InboxID = createdInbox.ID channelRecord.InboxID = createdInbox.ID if err := h.ttChannelSvc.Update(ctx, channelRecord); err != nil { applogger.L().Errorf("Failed to update TikTok channel inbox_id: %v", err) // Non-critical: inbox was created, channel link can be repaired } // 4. Call ttProvider.OnCreate for webhook setup // Note: OnCreate expects a ChannelConfig map; we build one from request data. ttConfig := map[string]interface{}{ "tiktok_business_id": req.TikTokBusinessID, "access_token": req.AccessToken, } updatedConfig, onCreateErr := h.ttProvider.OnCreate(ctx, createdInbox, ttConfig) if onCreateErr != nil { applogger.L().Warnf("TikTok OnCreate webhook setup failed: %v", onCreateErr) // Non-critical: channel + inbox created, webhook can be set up later } else { // Persist updated config (e.g. webhook_verify_token) if provider returned one if verifyToken, ok := updatedConfig["webhook_verify_token"]; ok { channelRecord.WebhookVerifyToken = verifyToken.(string) if err := h.ttChannelSvc.Update(ctx, channelRecord); err != nil { applogger.L().Warnf("Failed to persist TikTok webhook_verify_token: %v", err) } } } c.JSON(http.StatusCreated, gin.H{ "channel": gin.H{ "id": channelRecord.ID, "account_id": channelRecord.AccountID, "inbox_id": channelRecord.InboxID, "tiktok_business_id": channelRecord.TikTokBusinessID, }, "inbox": gin.H{ "id": createdInbox.ID, "name": createdInbox.Name, "channel_type": createdInbox.ChannelType, "channel_id": createdInbox.ChannelID, "enabled": createdInbox.Enabled, "enable_auto_assignment": createdInbox.EnableAutoAssignment, }, }) } // GetTikTokChannel retrieves a TikTok channel by ID. // GET /api/v1/accounts/:id/channels/tiktok_channel/:tt_id func (h *TikTokChannelHandler) GetTikTokChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ttIDStr := c.Param("tt_id") ttID, err := strconv.ParseUint(ttIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tt_id"}) return } ctx := c.Request.Context() ch, err := h.ttChannelSvc.GetByID(ctx, uint(ttID)) if err != nil { applogger.L().Errorf("Failed to get TikTok channel: %v", err) c.JSON(http.StatusNotFound, gin.H{"error": "TikTok channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "TikTok channel does not belong to this account"}) return } c.JSON(http.StatusOK, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "tiktok_business_id": ch.TikTokBusinessID, "reauthorization_required": ch.ReauthorizationRequired, }) } // UpdateTikTokChannelRequest is the DTO for updating a TikTok channel. type UpdateTikTokChannelRequest struct { Name string `json:"name,omitempty"` AccessToken string `json:"access_token,omitempty"` InboxName string `json:"inbox_name,omitempty"` EnableAutoAssignment *bool `json:"enable_auto_assignment,omitempty"` } // UpdateTikTokChannel updates a TikTok channel configuration. // PATCH /api/v1/accounts/:id/channels/tiktok_channel/:tt_id func (h *TikTokChannelHandler) UpdateTikTokChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ttIDStr := c.Param("tt_id") ttID, err := strconv.ParseUint(ttIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tt_id"}) return } var req UpdateTikTokChannelRequest if err := c.ShouldBindJSON(&req); err != nil { applogger.L().Errorf("Failed to bind update tiktok channel request: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() ch, err := h.ttChannelSvc.GetByID(ctx, uint(ttID)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "TikTok channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "TikTok channel does not belong to this account"}) return } // Apply updates if req.AccessToken != "" { ch.AccessToken = req.AccessToken } if err := h.ttChannelSvc.Update(ctx, ch); err != nil { applogger.L().Errorf("Failed to update TikTok channel: %v", err) c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update TikTok channel"}) return } // Update inbox name if provided if req.InboxName != "" && ch.InboxID > 0 { updateReq := service.UpdateInboxRequest{ Name: req.InboxName, } if req.EnableAutoAssignment != nil { updateReq.EnableAutoAssignment = req.EnableAutoAssignment } if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil { applogger.L().Warnf("Failed to update TikTok inbox: %v", inboxErr) } } c.JSON(http.StatusOK, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "tiktok_business_id": ch.TikTokBusinessID, "message": "TikTok channel updated successfully", }) } // DeleteTikTokChannel removes a TikTok channel and its associated inbox. // DELETE /api/v1/accounts/:id/channels/tiktok_channel/:tt_id func (h *TikTokChannelHandler) DeleteTikTokChannel(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ttIDStr := c.Param("tt_id") ttID, err := strconv.ParseUint(ttIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tt_id"}) return } ctx := c.Request.Context() ch, err := h.ttChannelSvc.GetByID(ctx, uint(ttID)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "TikTok channel not found"}) return } // Verify account ownership if ch.AccountID != uint(accountID) { c.JSON(http.StatusForbidden, gin.H{"error": "TikTok channel does not belong to this account"}) return } // Delete the channel record if err := h.ttChannelSvc.Delete(ctx, uint(ttID)); err != nil { applogger.L().Errorf("Failed to delete TikTok channel: %v", err) c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete TikTok channel"}) return } // Delete the associated inbox (if exists) if ch.InboxID > 0 { if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil { applogger.L().Warnf("Failed to delete inbox for TikTok channel: %v", delErr) } } c.JSON(http.StatusOK, gin.H{"message": "TikTok channel deleted successfully"}) } // ListTikTokChannels retrieves all TikTok channels for an account. // GET /api/v1/accounts/:id/channels/tiktok_channel func (h *TikTokChannelHandler) ListTikTokChannels(c *gin.Context) { accountIDStr := c.Param("id") accountID, err := strconv.ParseUint(accountIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"}) return } ctx := c.Request.Context() channels, err := h.ttChannelSvc.ListByAccount(ctx, uint(accountID)) if err != nil { applogger.L().Errorf("Failed to list TikTok channels: %v", err) c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list TikTok channels"}) return } result := make([]gin.H, 0, len(channels)) for _, ch := range channels { result = append(result, gin.H{ "id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "tiktok_business_id": ch.TikTokBusinessID, "reauthorization_required": ch.ReauthorizationRequired, }) } c.JSON(http.StatusOK, gin.H{ "channels": result, "count": len(result), }) }