164 lines
5.8 KiB
Go
164 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/segmentio/kafka-go"
|
|
"github.com/tinode/chat/server/logs"
|
|
"github.com/tinode/chat/server/store"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type kafkaExternalWorkbenchEvent struct {
|
|
EventID string `json:"event_id"`
|
|
TraceID string `json:"trace_id,omitempty"`
|
|
TenantID types.TenantID `json:"tenant_id,omitempty"`
|
|
TenantCode string `json:"tenant_code,omitempty"`
|
|
ActorUserID int64 `json:"actor_user_id"`
|
|
Module string `json:"module"`
|
|
Action string `json:"action"`
|
|
ResourceID string `json:"resource_id"`
|
|
TargetUserIDs []int64 `json:"target_user_ids,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
OccurredAt time.Time `json:"occurred_at"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
func startKafkaWorkbenchEventConsumer() {
|
|
startKafkaConsumer("workbench_external", handleKafkaWorkbenchEvent)
|
|
}
|
|
|
|
func handleKafkaWorkbenchEvent(ctx context.Context, envelope kafkaEnvelope, msg kafka.Message) error {
|
|
payload, normalized, err := decodeKafkaWorkbenchEvent(envelope, msg)
|
|
if err != nil {
|
|
logs.Warn.Printf("kafka workbench event malformed: topic=%s offset=%d err=%v", msg.Topic, msg.Offset, err)
|
|
return err
|
|
}
|
|
tenantID, err := kafkaTenantIDFromEnvelope(normalized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
consume, claimed, err := beginKafkaConsumeForTopic(tenantID, normalized, "workbench_external")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !claimed {
|
|
logs.Info.Printf("kafka workbench event skipped: event=%s group=%s status=%s", normalized.EventID, kafkaBus.config.ConsumerGroup, consume.Status)
|
|
return nil
|
|
}
|
|
if err = executeKafkaExternalWorkbenchEvent(tenantID, normalized, payload); err != nil {
|
|
reason := kafkaCommandErrorReason(err)
|
|
failed, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(tenantID, normalized.EventID, kafkaBus.config.ConsumerGroup, reason, err.Error(), time.Now().UTC().Round(time.Millisecond))
|
|
if markErr != nil {
|
|
return markErr
|
|
}
|
|
if failed != nil && failed.RetryCount >= kafkaBus.config.MaxRetryCount {
|
|
return markKafkaDeadLetter(ctx, tenantID, normalized, failed, reason, err.Error())
|
|
}
|
|
if failed != nil {
|
|
sleepKafkaRetryBackoff(failed.RetryCount)
|
|
}
|
|
return err
|
|
}
|
|
return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(tenantID, normalized.EventID, kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond))
|
|
}
|
|
|
|
func decodeKafkaWorkbenchEvent(envelope kafkaEnvelope, msg kafka.Message) (kafkaExternalWorkbenchEvent, kafkaEnvelope, error) {
|
|
var payload kafkaExternalWorkbenchEvent
|
|
if len(envelope.Payload) > 0 {
|
|
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
|
|
return payload, envelope, err
|
|
}
|
|
} else if err := json.Unmarshal(msg.Value, &payload); err != nil {
|
|
return payload, envelope, err
|
|
}
|
|
if strings.TrimSpace(payload.EventID) == "" {
|
|
payload.EventID = envelope.EventID
|
|
}
|
|
if strings.TrimSpace(payload.Action) == "" && envelope.EventType != "" {
|
|
payload.Action = strings.TrimPrefix(envelope.EventType, "workbench.")
|
|
}
|
|
if strings.TrimSpace(payload.EventID) == "" || strings.TrimSpace(payload.Action) == "" || strings.TrimSpace(payload.ResourceID) == "" {
|
|
return payload, envelope, types.ErrMalformed
|
|
}
|
|
if payload.OccurredAt.IsZero() {
|
|
payload.OccurredAt = time.Now().UTC().Round(time.Millisecond)
|
|
}
|
|
normalized := kafkaEnvelope{
|
|
EventID: payload.EventID,
|
|
TraceID: firstNonEmpty(payload.TraceID, envelope.TraceID),
|
|
TenantCode: firstNonEmpty(payload.TenantCode, envelope.TenantCode),
|
|
TenantID: firstTenantID(payload.TenantID, envelope.TenantID),
|
|
SourceService: firstNonEmpty(envelope.SourceService, "im-workbench"),
|
|
TargetService: firstNonEmpty(envelope.TargetService, "im-server"),
|
|
EventType: "workbench." + payload.Action,
|
|
BizType: firstNonEmpty(payload.Module, envelope.BizType, "workbench"),
|
|
BizID: firstNonEmpty(payload.ResourceID, envelope.BizID),
|
|
OccurredAt: payload.OccurredAt,
|
|
Payload: msg.Value,
|
|
}
|
|
if payload.ActorUserID > 0 {
|
|
normalized.Actor = &kafkaActor{Type: "user", ID: types.Uid(payload.ActorUserID).UserId()}
|
|
}
|
|
return payload, normalized, nil
|
|
}
|
|
|
|
func executeKafkaExternalWorkbenchEvent(tenantID types.TenantID, envelope kafkaEnvelope, payload kafkaExternalWorkbenchEvent) error {
|
|
if store.Security == nil {
|
|
return types.ErrUnsupported
|
|
}
|
|
actor := types.Uid(payload.ActorUserID)
|
|
data := types.KVMap{
|
|
"module": payload.Module,
|
|
"resource_id": payload.ResourceID,
|
|
}
|
|
for k, v := range payload.Data {
|
|
data[k] = v
|
|
}
|
|
targets := compactWorkbenchTargets(payload.TargetUserIDs, actor)
|
|
for _, target := range targets {
|
|
targetID := target.UserId()
|
|
eventID := stableIMEventID("workbench", envelope.EventID, targetID)
|
|
if target != actor && payload.Title != "" {
|
|
if err := createIMSystemNotification(tenantID, target, stableIMEventID(eventID, "notification"), payload.Action,
|
|
payload.Title, payload.Summary, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", targetID, actor, payload.Action, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func compactWorkbenchTargets(raw []int64, actor types.Uid) []types.Uid {
|
|
seen := make(map[types.Uid]bool, len(raw)+1)
|
|
targets := make([]types.Uid, 0, len(raw)+1)
|
|
for _, value := range raw {
|
|
uid := types.Uid(value)
|
|
if uid.IsZero() || seen[uid] {
|
|
continue
|
|
}
|
|
seen[uid] = true
|
|
targets = append(targets, uid)
|
|
}
|
|
if len(targets) == 0 && !actor.IsZero() {
|
|
targets = append(targets, actor)
|
|
}
|
|
return targets
|
|
}
|
|
|
|
func firstTenantID(values ...types.TenantID) types.TenantID {
|
|
for _, value := range values {
|
|
if !value.IsZero() {
|
|
return value
|
|
}
|
|
}
|
|
return types.ZeroTenantID
|
|
}
|