165 lines
5.6 KiB
Go
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)
|
|
}
|