608 lines
22 KiB
Go
608 lines
22 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
accountdomain "git.ipao.vip/rogee/creator-hub/internal/account"
|
|
"git.ipao.vip/rogee/creator-hub/internal/creator"
|
|
hub "git.ipao.vip/rogee/creator-hub/internal/environment"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const creatorEventReconcileInterval = 10 * time.Second
|
|
|
|
type creatorGatewayEvent struct {
|
|
DeliveryID string `json:"delivery_id,omitempty"`
|
|
Generation string `json:"generation,omitempty"`
|
|
Kind string `json:"kind"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Continuity string `json:"continuity,omitempty"`
|
|
BoundaryAt string `json:"boundary_at,omitempty"`
|
|
BoundarySource string `json:"boundary_source,omitempty"`
|
|
Baseline bool `json:"baseline,omitempty"`
|
|
Notice *creatorGatewayEventNotice `json:"notice,omitempty"`
|
|
}
|
|
|
|
type creatorGatewayEventNotice struct {
|
|
EventKey string `json:"event_key"`
|
|
EventType string `json:"event_type"`
|
|
InteractorUID string `json:"interactor_uid"`
|
|
CommentID string `json:"comment_id"`
|
|
WorkID string `json:"work_id"`
|
|
MessageType string `json:"message_type,omitempty"`
|
|
MessageText string `json:"message_text,omitempty"`
|
|
PlatformEventAt string `json:"platform_event_at,omitempty"`
|
|
GatewayReceivedAt string `json:"gateway_received_at,omitempty"`
|
|
}
|
|
|
|
type creatorEventBinding struct {
|
|
accountID string
|
|
uid string
|
|
env hub.EnvironmentContext
|
|
gateway hub.Gateway
|
|
hubStore *hub.Store
|
|
sessionToken string
|
|
}
|
|
|
|
type creatorUpdateHub struct {
|
|
mu sync.Mutex
|
|
subscribers map[chan struct{}]struct{}
|
|
}
|
|
|
|
var creatorUpdates = &creatorUpdateHub{subscribers: make(map[chan struct{}]struct{})}
|
|
|
|
func (h *creatorUpdateHub) subscribe() (<-chan struct{}, func()) {
|
|
channel := make(chan struct{}, 1)
|
|
h.mu.Lock()
|
|
h.subscribers[channel] = struct{}{}
|
|
h.mu.Unlock()
|
|
return channel, func() {
|
|
h.mu.Lock()
|
|
if _, ok := h.subscribers[channel]; ok {
|
|
delete(h.subscribers, channel)
|
|
close(channel)
|
|
}
|
|
h.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (h *creatorUpdateHub) publish() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
for channel := range h.subscribers {
|
|
select {
|
|
case channel <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func creatorListenerGeneration(env hub.EnvironmentContext) string {
|
|
return fmt.Sprintf("%s:%s:%d", env.RuntimeID, env.RuntimeNetworkID, env.BindingVersion)
|
|
}
|
|
|
|
var creatorListenerSessionNonce atomic.Uint64
|
|
|
|
func creatorListenerSessionToken(env hub.EnvironmentContext) string {
|
|
return fmt.Sprintf("%s/%d-%d", creatorListenerGeneration(env), time.Now().UnixNano(), creatorListenerSessionNonce.Add(1))
|
|
}
|
|
|
|
func listenerBoundaryPointer(value time.Time) *time.Time {
|
|
if value.IsZero() {
|
|
return nil
|
|
}
|
|
value = value.UTC()
|
|
return &value
|
|
}
|
|
|
|
func persistCreatorListenerState(ctx context.Context, store *creator.Store, binding creatorEventBinding, status, reason string, boundaryAt *time.Time, deliveryID string) {
|
|
if store == nil {
|
|
return
|
|
}
|
|
if _, err := store.UpsertListenerState(ctx, creator.ListenerState{
|
|
AccountID: binding.accountID,
|
|
Platform: creator.PlatformDouyin,
|
|
Generation: creatorListenerGeneration(binding.env),
|
|
SessionToken: binding.sessionToken,
|
|
Status: status,
|
|
BoundaryAt: boundaryAt,
|
|
LastDeliveryID: deliveryID,
|
|
Reason: reason,
|
|
}); err != nil {
|
|
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator listener state persistence failed")
|
|
}
|
|
}
|
|
|
|
func (binding creatorEventBinding) key() string {
|
|
return fmt.Sprintf("%s\x00%s\x00%s\x00%d\x00%s\x00%s\x00%s\x00%s", binding.gateway.Name, binding.gateway.Endpoint, binding.gateway.Token, binding.env.BindingVersion, binding.env.RuntimeID, binding.env.RuntimeNetworkID, binding.env.Exit.ID, binding.uid)
|
|
}
|
|
|
|
type creatorEventListenerHandle struct {
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
key string
|
|
}
|
|
|
|
type creatorEventListenerManager struct {
|
|
mu sync.Mutex
|
|
items map[string]creatorEventListenerHandle
|
|
}
|
|
|
|
func RunCreatorEventListeners(ctx context.Context, store *creator.Store, phaseAStore *accountdomain.Store, hubStore *hub.Store, executor creator.ActionExecutor, generator creator.TextGenerator) {
|
|
manager := &creatorEventListenerManager{items: map[string]creatorEventListenerHandle{}}
|
|
ticker := time.NewTicker(creatorEventReconcileInterval)
|
|
defer ticker.Stop()
|
|
defer manager.close()
|
|
for {
|
|
if err := manager.reconcile(ctx, store, phaseAStore, hubStore, executor, generator); err != nil && ctx.Err() == nil {
|
|
logrus.WithField("service", "control-plane").WithError(err).Warn("creator event listener reconciliation failed")
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (manager *creatorEventListenerManager) reconcile(ctx context.Context, store *creator.Store, phaseAStore *accountdomain.Store, hubStore *hub.Store, executor creator.ActionExecutor, generator creator.TextGenerator) error {
|
|
if store == nil || phaseAStore == nil || hubStore == nil {
|
|
return creator.ErrUnavailable
|
|
}
|
|
if recovered, err := store.RecoverStaleProcessing(ctx, time.Now().UTC()); err != nil {
|
|
return err
|
|
} else if recovered > 0 {
|
|
logrus.WithField("count", recovered).Warn("recovered stale creator operations as uncertain")
|
|
}
|
|
accounts, err := phaseAStore.ListAccounts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desired := make(map[string]creatorEventBinding)
|
|
for _, account := range accounts {
|
|
if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" {
|
|
continue
|
|
}
|
|
profile, profileErr := store.GetAccountProfile(ctx, account.ID)
|
|
if profileErr != nil {
|
|
logrus.WithError(profileErr).WithField("account_id", account.ID).Warn("creator event listener account profile unavailable")
|
|
continue
|
|
}
|
|
if profile.LoginStatus != "logged_in" {
|
|
continue
|
|
}
|
|
if !creatorEventUID(profile.PlatformAccountKey) {
|
|
logrus.WithField("account_id", account.ID).Warn("creator event listener account UID is unavailable")
|
|
continue
|
|
}
|
|
environment, environmentErr := hubStore.GetEnvironmentContextForAccount(ctx, account.ID)
|
|
if environmentErr != nil {
|
|
logrus.WithError(environmentErr).WithField("account_id", account.ID).Warn("creator event listener environment unavailable")
|
|
continue
|
|
}
|
|
if environment.RuntimeID == "" || environment.RuntimeNetworkID == "" || environment.BindingVersion <= 0 {
|
|
continue
|
|
}
|
|
gateway, gatewayErr := hubStore.GetGateway(ctx, environment.Gateway)
|
|
if gatewayErr != nil {
|
|
logrus.WithError(gatewayErr).WithField("account_id", account.ID).Warn("creator event listener gateway unavailable")
|
|
continue
|
|
}
|
|
desired[account.ID] = creatorEventBinding{accountID: account.ID, uid: profile.PlatformAccountKey, env: environment, gateway: gateway, hubStore: hubStore}
|
|
}
|
|
|
|
var stopping []creatorEventListenerHandle
|
|
manager.mu.Lock()
|
|
for accountID, current := range manager.items {
|
|
binding, ok := desired[accountID]
|
|
if ok && current.key == binding.key() {
|
|
state, stateErr := store.GetListenerState(ctx, accountID)
|
|
if stateErr != nil || !state.Invalidated {
|
|
continue
|
|
}
|
|
}
|
|
current.cancel()
|
|
delete(manager.items, accountID)
|
|
stopping = append(stopping, current)
|
|
}
|
|
manager.mu.Unlock()
|
|
for _, current := range stopping {
|
|
<-current.done
|
|
}
|
|
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
for accountID, binding := range desired {
|
|
if _, ok := manager.items[accountID]; ok {
|
|
continue
|
|
}
|
|
listenerContext, cancel := context.WithCancel(ctx)
|
|
done := make(chan struct{})
|
|
listenerBinding := binding
|
|
manager.items[accountID] = creatorEventListenerHandle{cancel: cancel, done: done, key: binding.key()}
|
|
go func() {
|
|
defer close(done)
|
|
runCreatorEventListener(listenerContext, store, listenerBinding, executor, generator)
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (manager *creatorEventListenerManager) close() {
|
|
manager.mu.Lock()
|
|
handles := make([]creatorEventListenerHandle, 0, len(manager.items))
|
|
for accountID, current := range manager.items {
|
|
current.cancel()
|
|
handles = append(handles, current)
|
|
delete(manager.items, accountID)
|
|
}
|
|
manager.mu.Unlock()
|
|
for _, current := range handles {
|
|
<-current.done
|
|
}
|
|
}
|
|
|
|
func runCreatorEventListener(ctx context.Context, store *creator.Store, binding creatorEventBinding, executor creator.ActionExecutor, generator creator.TextGenerator) {
|
|
binding.sessionToken = creatorListenerSessionToken(binding.env)
|
|
path := "/v1/browsers/" + url.PathEscape(binding.env.Alias) + "/douyin/events"
|
|
generation := gatewayGenerationPayload(binding.env)
|
|
useCtx, runtimeUse, err := beginRuntimeUseForEnvironment(ctx, binding.hubStore, binding.env, "listener", "creator-listener-"+binding.accountID)
|
|
if err != nil {
|
|
persistCreatorListenerState(context.WithoutCancel(ctx), store, binding, "error", "runtime use unavailable: "+err.Error(), nil, "")
|
|
return
|
|
}
|
|
ctx = useCtx
|
|
defer func() {
|
|
if releaseErr := runtimeUse.Close(); releaseErr != nil {
|
|
logrus.WithError(releaseErr).WithField("account_id", binding.accountID).Error("creator listener runtime use release failed")
|
|
}
|
|
}()
|
|
startPayload := make(map[string]any, len(generation)+1)
|
|
for key, value := range generation {
|
|
startPayload[key] = value
|
|
}
|
|
startPayload["expected_uid"] = binding.uid
|
|
persistCreatorListenerState(ctx, store, binding, "starting", "等待平台边界标记", nil, "")
|
|
defer func() {
|
|
stopCreatorEventListener(binding.accountID, binding.gateway, path, generation)
|
|
persistCreatorListenerState(context.WithoutCancel(ctx), store, binding, "stopped", "监听已停止", nil, "")
|
|
}()
|
|
|
|
backoff := time.Second
|
|
for ctx.Err() == nil {
|
|
status, _, err := gatewayCall(ctx, binding.gateway, http.MethodPost, path, startPayload, 30*time.Second)
|
|
if err != nil || status != http.StatusOK {
|
|
if err == nil {
|
|
err = fmt.Errorf("gateway returned HTTP %d", status)
|
|
}
|
|
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener start failed")
|
|
persistCreatorListenerState(ctx, store, binding, "error", err.Error(), nil, "")
|
|
if !waitCreatorEventBackoff(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > 30*time.Second {
|
|
backoff = 30 * time.Second
|
|
}
|
|
continue
|
|
}
|
|
backoff = time.Second
|
|
// Every successful start creates a new boundary. Events observed before
|
|
// its marker remain historical even when the previous poll loop was ready.
|
|
ready := false
|
|
var boundaryAt time.Time
|
|
for ctx.Err() == nil {
|
|
status, body, err := gatewayCall(ctx, binding.gateway, http.MethodGet, path+"?limit=100&wait=25", generation, 35*time.Second)
|
|
if err != nil || status != http.StatusOK {
|
|
if err == nil {
|
|
err = fmt.Errorf("gateway returned HTTP %d", status)
|
|
}
|
|
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener poll failed")
|
|
persistCreatorListenerState(ctx, store, binding, "gap", err.Error(), listenerBoundaryPointer(boundaryAt), "")
|
|
break
|
|
}
|
|
var events []creatorGatewayEvent
|
|
if err := json.Unmarshal(body, &events); err != nil {
|
|
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener response is invalid")
|
|
persistCreatorListenerState(ctx, store, binding, "gap", "监听响应无法解析: "+err.Error(), listenerBoundaryPointer(boundaryAt), "")
|
|
break
|
|
}
|
|
for _, event := range events {
|
|
if event.Kind == "baseline" {
|
|
parsedBoundary, boundaryReady, reason := creatorGatewayBoundary(event)
|
|
boundaryAt, ready = parsedBoundary, boundaryReady
|
|
status := "gap"
|
|
if ready {
|
|
status, reason = "ready", ""
|
|
}
|
|
persistCreatorListenerState(ctx, store, binding, status, reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
|
|
} else if event.Kind == "open" || event.Kind == "error" || event.Kind == "close" || event.Kind == "reconnected" {
|
|
// A transport event never proves continuity. Only the explicit
|
|
// boundary marker permits automatic writes again.
|
|
ready = false
|
|
persistCreatorListenerState(ctx, store, binding, "gap", event.Reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
|
|
}
|
|
if event.Kind == "notice" {
|
|
if needsBaseline, reason := creatorGatewayEventNeedsBaseline(event, ready); needsBaseline {
|
|
event.Baseline = true
|
|
event.Reason = reason
|
|
} else if creatorEventBeforeBoundary(event, boundaryAt) {
|
|
event.Baseline = true
|
|
event.Reason = "平台事件早于监听边界"
|
|
}
|
|
status := "gap"
|
|
reason := event.Reason
|
|
if ready && !event.Baseline {
|
|
status, reason = "ready", ""
|
|
}
|
|
persistCreatorListenerState(ctx, store, binding, status, reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
|
|
}
|
|
handleCreatorGatewayEvent(ctx, store, binding, event, executor, generator)
|
|
}
|
|
}
|
|
if !waitCreatorEventBackoff(ctx, backoff) {
|
|
return
|
|
}
|
|
backoff *= 2
|
|
if backoff > 30*time.Second {
|
|
backoff = 30 * time.Second
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopCreatorEventListener(accountID string, gateway hub.Gateway, path string, generation map[string]any) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if status, _, err := gatewayCall(ctx, gateway, http.MethodDelete, path, generation, 5*time.Second); err != nil || status != http.StatusNoContent {
|
|
if err == nil {
|
|
err = fmt.Errorf("gateway returned HTTP %d", status)
|
|
}
|
|
logrus.WithError(err).WithField("account_id", accountID).Warn("creator event listener stop failed")
|
|
}
|
|
}
|
|
|
|
func waitCreatorEventBackoff(ctx context.Context, delay time.Duration) bool {
|
|
timer := time.NewTimer(delay)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-timer.C:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func pathForCreatorEvent(environment hub.EnvironmentContext) string {
|
|
return "/v1/browsers/" + url.PathEscape(environment.Alias) + "/douyin/events"
|
|
}
|
|
|
|
func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, binding creatorEventBinding, event creatorGatewayEvent, executor creator.ActionExecutor, generator creator.TextGenerator) {
|
|
ack := func() {
|
|
if event.DeliveryID == "" {
|
|
return
|
|
}
|
|
// Acknowledgement is deliberately detached from the listener poll loop:
|
|
// receipt and event classification must not be serialized behind a slow
|
|
// gateway request. The gateway keeps the delivery until this succeeds.
|
|
go func(deliveryID string) {
|
|
ackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
defer cancel()
|
|
ackPath := pathForCreatorEvent(binding.env)
|
|
status, _, ackErr := gatewayCall(ackCtx, binding.gateway, http.MethodGet, ackPath+"?ack="+url.QueryEscape(deliveryID)+"&limit=1&wait=0", gatewayGenerationPayload(binding.env), 5*time.Second)
|
|
if ackErr != nil || status != http.StatusOK {
|
|
if ackErr == nil {
|
|
ackErr = fmt.Errorf("gateway returned HTTP %d", status)
|
|
}
|
|
logrus.WithError(ackErr).WithField("account_id", binding.accountID).Warn("creator event acknowledgement failed")
|
|
}
|
|
}(event.DeliveryID)
|
|
}
|
|
switch event.Kind {
|
|
case "error":
|
|
logrus.WithFields(logrus.Fields{"account_id": binding.accountID, "reason": event.Reason, "continuity": event.Continuity}).Warn("creator event listener reported an error")
|
|
ack()
|
|
return
|
|
case "reconnected":
|
|
logrus.WithField("account_id", binding.accountID).Warn("creator event listener reconnected")
|
|
ack()
|
|
return
|
|
case "open", "baseline":
|
|
ack()
|
|
return
|
|
case "close":
|
|
logrus.WithField("account_id", binding.accountID).Warn("creator event listener connection closed")
|
|
ack()
|
|
return
|
|
case "notice":
|
|
default:
|
|
logrus.WithFields(logrus.Fields{"account_id": binding.accountID, "kind": event.Kind}).Warn("creator event listener returned an unknown event")
|
|
ack()
|
|
return
|
|
}
|
|
if event.Notice == nil {
|
|
logrus.WithField("account_id", binding.accountID).Warn("creator event listener notice is missing")
|
|
ack()
|
|
return
|
|
}
|
|
input, err := creatorEventFromGatewayNotice(binding.accountID, *event.Notice)
|
|
if err != nil {
|
|
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener notice was rejected")
|
|
ack()
|
|
return
|
|
}
|
|
input.Baseline = event.Baseline
|
|
input.BaselineReason = event.Reason
|
|
input.Generation = strings.TrimSpace(event.Generation)
|
|
if input.Generation == "" {
|
|
input.Generation = creatorListenerGeneration(binding.env)
|
|
}
|
|
selfEvent := binding.uid != "" && input.InteractorUID == binding.uid
|
|
if selfEvent {
|
|
input.Baseline = true
|
|
input.BaselineReason = "接收账号主动行为"
|
|
}
|
|
if input.GatewayReceivedAt == nil {
|
|
receivedAt := time.Now().UTC()
|
|
input.GatewayReceivedAt = &receivedAt
|
|
}
|
|
if input.PlatformEventAt == nil {
|
|
input.Baseline = true
|
|
input.BaselineReason = "缺少平台事件时间"
|
|
}
|
|
if store == nil {
|
|
return
|
|
}
|
|
// Receipt is durable before the potentially slow action. This prevents an
|
|
// executor outage from erasing the platform notification and lets polling
|
|
// continue while a prior action is still in flight.
|
|
received, err := store.RecordEvent(ctx, input)
|
|
if err != nil {
|
|
logrus.WithError(err).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator event receipt failed")
|
|
return
|
|
}
|
|
creatorUpdates.publish()
|
|
if input.EventType == "dm" && input.InteractorUID != "" {
|
|
messageAt := input.PlatformEventAt
|
|
if messageAt == nil {
|
|
receivedAt := input.ReceivedAt
|
|
if receivedAt.IsZero() {
|
|
receivedAt = time.Now().UTC()
|
|
}
|
|
messageAt = &receivedAt
|
|
}
|
|
direction, sentState := "inbound", "received"
|
|
if selfEvent {
|
|
direction, sentState = "outbound", "succeeded"
|
|
}
|
|
savedMessage, _, messageErr := store.SaveMessage(ctx, creator.MessageInput{Platform: input.Platform, AccountID: input.ReceivingAccountID, PeerUID: input.InteractorUID, PlatformMessageKey: input.EventKey, Direction: direction, MessageType: input.MessageType, Text: input.MessageText, SentState: sentState, MessageAt: messageAt})
|
|
if messageErr != nil {
|
|
logrus.WithError(messageErr).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator direct message persistence failed")
|
|
return
|
|
}
|
|
if selfEvent {
|
|
if err := store.LinkMessageOperation(ctx, savedMessage.ID, input.EventKey); err != nil {
|
|
logrus.WithError(err).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator outbound direct message correlation failed")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// The event is now durable. An action may be slow or unavailable, but that
|
|
// must not hold ingestion or cause the same receipt to be fetched forever.
|
|
ack()
|
|
if input.Baseline || received.Event.State != "received" {
|
|
return
|
|
}
|
|
go func() {
|
|
result, processErr := store.ProcessAutomaticEvent(ctx, input, executor, generator)
|
|
if processErr != nil {
|
|
logrus.WithError(processErr).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator event processing failed")
|
|
return
|
|
}
|
|
logrus.WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey, "event_type": input.EventType, "state": result.Event.State}).Info("creator event processed")
|
|
creatorUpdates.publish()
|
|
}()
|
|
}
|
|
|
|
func creatorGatewayBoundary(event creatorGatewayEvent) (time.Time, bool, string) {
|
|
if event.BoundarySource != "douyin_identity_extra_now" {
|
|
return time.Time{}, false, "平台边界来源未验证"
|
|
}
|
|
if event.BoundaryAt == "" {
|
|
return time.Time{}, false, "平台边界无效"
|
|
}
|
|
parsedBoundary, err := time.Parse(time.RFC3339Nano, event.BoundaryAt)
|
|
if err != nil {
|
|
return time.Time{}, false, "平台边界无效"
|
|
}
|
|
return parsedBoundary.UTC(), true, ""
|
|
}
|
|
|
|
func creatorEventBeforeBoundary(event creatorGatewayEvent, boundaryAt time.Time) bool {
|
|
if boundaryAt.IsZero() || event.Notice == nil {
|
|
return false
|
|
}
|
|
platformAt, err := time.Parse(time.RFC3339Nano, event.Notice.PlatformEventAt)
|
|
return err != nil || !platformAt.After(boundaryAt)
|
|
}
|
|
|
|
func creatorGatewayEventNeedsBaseline(event creatorGatewayEvent, ready bool) (bool, string) {
|
|
if event.Baseline {
|
|
if event.Reason != "" {
|
|
return true, event.Reason
|
|
}
|
|
return true, "监听基线"
|
|
}
|
|
if !ready {
|
|
return true, "监听边界未确认"
|
|
}
|
|
if event.Notice == nil || strings.TrimSpace(event.Notice.PlatformEventAt) == "" {
|
|
return true, "缺少平台事件时间"
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
func creatorEventFromGatewayNotice(accountID string, notice creatorGatewayEventNotice) (creator.InteractionEvent, error) {
|
|
if accountID == "" || !creatorEventID(notice.EventKey) || !creator.ValidEventType(notice.EventType) {
|
|
return creator.InteractionEvent{}, creator.ErrInvalid
|
|
}
|
|
if notice.InteractorUID != "" && !creatorEventUID(notice.InteractorUID) {
|
|
return creator.InteractionEvent{}, creator.ErrInvalid
|
|
}
|
|
if (notice.CommentID != "" && !creatorEventID(notice.CommentID)) || (notice.WorkID != "" && !creatorEventID(notice.WorkID)) {
|
|
return creator.InteractionEvent{}, creator.ErrInvalid
|
|
}
|
|
messageType := strings.TrimSpace(notice.MessageType)
|
|
if messageType == "" {
|
|
messageType = creator.MessageTypeText
|
|
}
|
|
if !creator.ValidMessageType(messageType) {
|
|
return creator.InteractionEvent{}, creator.ErrInvalid
|
|
}
|
|
result := creator.InteractionEvent{Platform: creator.PlatformDouyin, ReceivingAccountID: accountID, EventKey: notice.EventKey, EventType: notice.EventType, InteractorUID: notice.InteractorUID, CommentID: notice.CommentID, WorkID: notice.WorkID, MessageType: messageType, MessageText: strings.TrimSpace(notice.MessageText)}
|
|
if strings.TrimSpace(notice.PlatformEventAt) != "" {
|
|
at, err := time.Parse(time.RFC3339Nano, notice.PlatformEventAt)
|
|
if err != nil {
|
|
return creator.InteractionEvent{}, fmt.Errorf("invalid platform event time: %w", err)
|
|
}
|
|
at = at.UTC()
|
|
result.PlatformEventAt = &at
|
|
}
|
|
if strings.TrimSpace(notice.GatewayReceivedAt) != "" {
|
|
receivedAt, err := time.Parse(time.RFC3339Nano, notice.GatewayReceivedAt)
|
|
if err != nil {
|
|
return creator.InteractionEvent{}, fmt.Errorf("invalid gateway receipt time: %w", err)
|
|
}
|
|
receivedAt = receivedAt.UTC()
|
|
result.GatewayReceivedAt = &receivedAt
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func creatorEventUID(value string) bool {
|
|
return creatorEventDigits(value, 20)
|
|
}
|
|
|
|
func creatorEventID(value string) bool {
|
|
return creatorEventDigits(value, 64)
|
|
}
|
|
|
|
func creatorEventDigits(value string, max int) bool {
|
|
if value == "" || len(value) > max || value[0] < '1' || value[0] > '9' {
|
|
return false
|
|
}
|
|
for _, char := range value[1:] {
|
|
if char < '0' || char > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|