feat: integrate creator hub douyin workflows
This commit is contained in:
@@ -34,6 +34,7 @@ type creatorGatewayEventNotice struct {
|
||||
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"`
|
||||
@@ -46,6 +47,68 @@ type creatorEventBinding struct {
|
||||
gateway hub.Gateway
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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),
|
||||
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)
|
||||
}
|
||||
@@ -180,7 +243,11 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
|
||||
startPayload[key] = value
|
||||
}
|
||||
startPayload["expected_uid"] = binding.uid
|
||||
defer stopCreatorEventListener(binding.accountID, binding.gateway, path, generation)
|
||||
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 {
|
||||
@@ -190,6 +257,7 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
|
||||
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
|
||||
}
|
||||
@@ -211,11 +279,13 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
|
||||
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 {
|
||||
@@ -236,10 +306,16 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
|
||||
ready = false
|
||||
boundaryAt = time.Time{}
|
||||
}
|
||||
status, reason := "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 {
|
||||
@@ -249,6 +325,12 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
|
||||
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)
|
||||
}
|
||||
@@ -360,6 +442,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
|
||||
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 {
|
||||
@@ -369,7 +452,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
|
||||
}
|
||||
messageAt = &receivedAt
|
||||
}
|
||||
if _, _, messageErr := store.SaveMessage(ctx, creator.MessageInput{Platform: input.Platform, AccountID: input.ReceivingAccountID, PeerUID: input.InteractorUID, PlatformMessageKey: "event:" + input.EventKey, Direction: "inbound", MessageType: "text", Text: input.MessageText, SentState: "received", MessageAt: messageAt}); messageErr != nil {
|
||||
if _, _, messageErr := store.SaveMessage(ctx, creator.MessageInput{Platform: input.Platform, AccountID: input.ReceivingAccountID, PeerUID: input.InteractorUID, PlatformMessageKey: "event:" + input.EventKey, Direction: "inbound", MessageType: input.MessageType, Text: input.MessageText, SentState: "received", MessageAt: messageAt}); messageErr != nil {
|
||||
logrus.WithError(messageErr).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator direct message persistence failed")
|
||||
return
|
||||
}
|
||||
@@ -387,6 +470,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
|
||||
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()
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -424,7 +508,14 @@ func creatorEventFromGatewayNotice(accountID string, notice creatorGatewayEventN
|
||||
if (notice.CommentID != "" && !creatorEventID(notice.CommentID)) || (notice.WorkID != "" && !creatorEventID(notice.WorkID)) {
|
||||
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, MessageText: strings.TrimSpace(notice.MessageText)}
|
||||
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 {
|
||||
@@ -438,7 +529,8 @@ func creatorEventFromGatewayNotice(accountID string, notice creatorGatewayEventN
|
||||
if err != nil {
|
||||
return creator.InteractionEvent{}, fmt.Errorf("invalid gateway receipt time: %w", err)
|
||||
}
|
||||
result.ReceivedAt = receivedAt.UTC()
|
||||
receivedAt = receivedAt.UTC()
|
||||
result.GatewayReceivedAt = &receivedAt
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user