Files
gochat/internal/channel/tiktok/pipeline.go
T

165 lines
5.6 KiB
Go

package tiktok
// Pipeline integration for TikTok channel with gochat's message processing system.
// Reference: gochat's Telegram pipeline architecture — same pattern adapted for TikTok.
//
// Flow:
// Incoming: TikTok webhook event → IncomingProcessor.ProcessUpdate → Contact + Conversation + Message
// Outgoing: outgoing Message → OutgoingProcessor → TikTok Business API
//
// This file provides the bridge between the TikTok provider and gochat's pipeline.
import (
"context"
"encoding/json"
"fmt"
"time"
channelpkg "github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
applogger "github.com/gochat/gochat/pkg/logger"
)
// === Incoming Pipeline Integration ===
// IncomingProcessor bridges TikTok webhook events to gochat's IncomingMessageProcessor.
// Reference: Telegram IncomingProcessor.ProcessUpdate — same pattern
//
// Process:
// 1. Receive TikTok webhook event
// 2. Transform raw payload into IncomingMessage (via provider)
// 3. Create/update Contact (from TikTok user profile)
// 4. Create/update Conversation (from conversation_id)
// 5. Create Message with content
type IncomingProcessor struct {
service *TikTokService
repo *Repository
}
// NewIncomingProcessor creates a TikTok incoming message processor.
func NewIncomingProcessor(service *TikTokService, repo *Repository) *IncomingProcessor {
return &IncomingProcessor{
service: service,
repo: repo,
}
}
// ProcessUpdate handles a TikTok webhook event by transforming it into an IncomingMessage
// and then processing it through gochat's pipeline stages.
func (p *IncomingProcessor) ProcessUpdate(ctx context.Context, inbox *model.Inbox, rawEvent TikTokWebhookEvent) (*channelpkg.IncomingMessage, error) {
incomingMsg, err := p.transformToIncomingMessage(ctx, inbox, rawEvent)
if err != nil {
return nil, fmt.Errorf("tiktok incoming pipeline: transform failed: %w", err)
}
if incomingMsg == nil {
return nil, nil // no processable content
}
applogger.L().Infof("TikTok incoming: channel=%s sender=%s source_id=%s inbox=%d",
incomingMsg.ChannelType, incomingMsg.SenderID, incomingMsg.SourceID, inbox.ID)
return incomingMsg, nil
}
// transformToIncomingMessage converts a raw TikTok webhook event into an IncomingMessage.
func (p *IncomingProcessor) transformToIncomingMessage(ctx context.Context, inbox *model.Inbox, rawEvent TikTokWebhookEvent) (*channelpkg.IncomingMessage, error) {
switch rawEvent.Type {
case "message.received":
return p.transformMessageReceived(ctx, inbox, rawEvent)
case "message.read":
// Read receipts don't generate IncomingMessages; they update message status
applogger.L().Infof("TikTok incoming: read receipt for biz_id=%s", rawEvent.BizID)
return nil, nil
default:
applogger.L().Infof("TikTok incoming: ignoring event type %s", rawEvent.Type)
return nil, nil
}
}
// transformMessageReceived converts a TikTok message.received event to an IncomingMessage.
func (p *IncomingProcessor) transformMessageReceived(ctx context.Context, inbox *model.Inbox, rawEvent TikTokWebhookEvent) (*channelpkg.IncomingMessage, error) {
payloadBytes, err := json.Marshal(rawEvent.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal TikTok message data: %w", err)
}
var payload TikTokMessagePayload
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, fmt.Errorf("failed to unmarshal TikTok message payload: %w", err)
}
msg := &channelpkg.IncomingMessage{
ChannelType: channelpkg.ChannelTikTok,
SourceID: payload.MessageID,
ConversationID: payload.ConversationID,
SenderID: payload.FromUserID,
SenderName: payload.FromUserID, // enriched later by GetContactProfile
SenderType: channelpkg.SenderContact,
Content: payload.Content,
ContentType: mapContentType(payload.ContentType),
InboxID: inbox.ID,
AccountID: inbox.AccountID,
ReceivedAt: time.Unix(payload.Timestamp, 0),
Extra: channelpkg.ChannelConfig{
"tiktok_business_id": rawEvent.BizID,
"to_user_id": payload.ToUserID,
},
}
return msg, nil
}
// === Outgoing Pipeline Integration ===
// OutgoingProcessor handles sending outgoing messages via TikTok Business API.
// Reference: Telegram OutgoingProcessor pattern
type OutgoingProcessor struct {
service *TikTokService
}
// NewOutgoingProcessor creates a TikTok outgoing message processor.
func NewOutgoingProcessor(service *TikTokService) *OutgoingProcessor {
return &OutgoingProcessor{service: service}
}
// SendMessage sends an outgoing message to a TikTok user.
// Returns the TikTok message ID for tracking.
func (p *OutgoingProcessor) SendMessage(ctx context.Context, accessToken, toUserID, content string) (string, error) {
resp, err := p.service.SendMessage(ctx, accessToken, toUserID, content)
if err != nil {
return "", fmt.Errorf("tiktok outgoing: failed to send message to %s: %w", toUserID, err)
}
return resp.MessageID, nil
}
// === Helpers ===
// mapContentType maps TikTok content_type to gochat's ContentType enum.
func mapContentType(tiktokType string) channelpkg.ContentType {
switch tiktokType {
case "text":
return channelpkg.ContentText
case "image":
return channelpkg.ContentImage
case "video":
return channelpkg.ContentVideo
case "file":
return channelpkg.ContentFile
case "audio":
return channelpkg.ContentAudio
default:
return channelpkg.ContentText
}
}
// getChannelFromInbox resolves the ChannelTikTok model from an Inbox.
func (p *IncomingProcessor) getChannelFromInbox(ctx context.Context, inbox *model.Inbox) (*channelmodel.ChannelTikTok, error) {
return p.repo.GetByInboxID(ctx, inbox.ID)
}