package main import ( "context" "encoding/base64" "encoding/json" "errors" "strconv" "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" ) const ( adminCommandGroupProfileUpdate = "group.profile.update" adminCommandGroupGovernUpdate = "group.governance.update" adminCommandGroupAdminSet = "group.admin.set" adminCommandGroupAdminRemove = "group.admin.remove" adminCommandGroupMemberMute = "group.member.mute" adminCommandGroupMemberUnmute = "group.member.unmute" adminCommandGroupMemberRemove = "group.member.remove" adminCommandGroupOwnerTransfer = "group.owner.transfer" adminCommandGroupDissolve = "group.dissolve" adminCommandUserBan = "user.ban" adminCommandUserUnban = "user.unban" adminCommandUserFreeze = "user.freeze" adminCommandUserUnfreeze = "user.unfreeze" adminCommandUserDeviceKick = "user.device.kick" adminCommandUserLogoutAll = "user.logout_all" adminCommandUserCancel = "user.account.cancel" adminCommandUserDelete = "user.account.delete" adminCommandUserPasswordReset = "user.password.reset" adminCommandMessageHide = "message.hide" adminCommandMessageDelete = "message.delete" adminCommandKafkaDeadReplay = "kafka.dead_letter.replay" adminCommandKafkaDeadIgnore = "kafka.dead_letter.ignore" adminCommandKafkaDeadResolve = "kafka.dead_letter.resolve" ) var publishKafkaDeadLetter = publishKafkaEnvelope type adminCommandPayload struct { CommandID string `json:"command_id"` ActorUserID string `json:"actor_user_id"` Topic string `json:"topic"` ConversationType string `json:"conversation_type"` SeqID int64 `json:"seq_id"` UserID string `json:"user_id"` UserIDs []string `json:"user_ids"` TargetID string `json:"target_user_id"` DeviceID string `json:"device_id"` PasswordHash string `json:"password_hash"` Version int `json:"version"` Reason string `json:"reason"` DeadID string `json:"dead_id"` Note string `json:"note"` Hard bool `json:"hard"` ExpiresAt *time.Time `json:"expires_at"` Profile groupProfilePatch `json:"profile"` Governance groupGovernancePatch `json:"governance"` } type groupProfilePatch struct { Name *string `json:"name"` AvatarFileID *string `json:"avatar_file_id"` Description *string `json:"description"` } type groupGovernancePatch struct { JoinApproval *bool `json:"join_approval"` MuteAll *bool `json:"mute_all"` MemberUploadFiles *bool `json:"member_upload_files"` MemberPrivateChat *bool `json:"member_private_chat"` MemberInvite *bool `json:"member_invite"` MaxMemberCount *int `json:"max_member_count"` MaxAdminCount *int `json:"max_admin_count"` } type adminCommandResultPayload struct { CommandID string `json:"command_id"` CommandEventID string `json:"command_event_id"` Status string `json:"status"` Reason string `json:"reason,omitempty"` Message string `json:"message,omitempty"` Result map[string]any `json:"result,omitempty"` } func startKafkaAdminCommandConsumer() { startKafkaConsumer("admin_command", handleKafkaAdminCommand) } func handleKafkaAdminCommand(ctx context.Context, envelope kafkaEnvelope, _ kafka.Message) error { if strings.TrimSpace(envelope.EventID) == "" { return publishKafkaAdminCommandResult(ctx, envelope, "", "failed", "MALFORMED_EVENT", "missing event_id", nil) } if strings.TrimSpace(envelope.EventType) == "" { return publishKafkaAdminCommandResult(ctx, envelope, "", "failed", "MALFORMED_EVENT", "missing event_type", nil) } var payload adminCommandPayload if len(envelope.Payload) > 0 { if err := json.Unmarshal(envelope.Payload, &payload); err != nil { return publishKafkaAdminCommandResult(ctx, envelope, "", "failed", "MALFORMED_PAYLOAD", err.Error(), nil) } } if payload.CommandID == "" { payload.CommandID = envelope.EventID } tenantID, tenantErr := kafkaTenantID(envelope.TenantCode) if tenantErr != nil { return publishKafkaAdminCommandResult(ctx, envelope, payload.CommandID, "failed", kafkaCommandErrorReason(tenantErr), tenantErr.Error(), nil) } consume, claimed, beginErr := beginKafkaConsumeForTopic(tenantID, envelope, "admin_command") if beginErr != nil { return beginErr } if !claimed { logs.Info.Printf("kafka admin command skipped: event=%s group=%s status=%s", envelope.EventID, kafkaBus.config.ConsumerGroup, consume.Status) return nil } result, execErr := executeKafkaAdminCommand(tenantID, envelope, payload) if execErr != nil { reason := kafkaCommandErrorReason(execErr) failedRecord, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(tenantID, envelope.EventID, kafkaBus.config.ConsumerGroup, reason, execErr.Error(), time.Now().UTC().Round(time.Millisecond)) if markErr != nil { return markErr } if shouldDeadLetterKafkaCommand(reason, failedRecord.RetryCount) { if deadErr := markKafkaDeadLetter(ctx, tenantID, envelope, failedRecord, reason, execErr.Error()); deadErr != nil { return deadErr } if err := publishKafkaAdminCommandResult(ctx, envelope, payload.CommandID, "dead", reason, execErr.Error(), result); err != nil { return err } logs.Warn.Printf("kafka admin command dead: event=%s type=%s reason=%s err=%v", envelope.EventID, envelope.EventType, reason, execErr) return nil } if kafkaCommandReasonRetryable(reason) { sleepKafkaRetryBackoff(failedRecord.RetryCount) return execErr } if err := publishKafkaAdminCommandResult(ctx, envelope, payload.CommandID, "failed", reason, execErr.Error(), result); err != nil { return err } logs.Warn.Printf("kafka admin command failed: event=%s type=%s reason=%s err=%v", envelope.EventID, envelope.EventType, reason, execErr) return nil } if err := store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(tenantID, envelope.EventID, kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond)); err != nil { return err } if err := publishKafkaAdminCommandResult(ctx, envelope, payload.CommandID, "succeeded", "", "", result); err != nil { return err } if err := enqueueKafkaAuditEvent(ctx, tenantID, "admin", parseKafkaCommandUser(payload.ActorUserID), envelope.EventType, "admin_command", payload.CommandID, "succeeded", "", map[string]any{ "topic": payload.Topic, "biz_id": envelope.BizID, "event_id": envelope.EventID, "result": result, }); err != nil { logs.Warn.Printf("kafka admin command audit enqueue failed: event=%s err=%v", envelope.EventID, err) } return nil } func executeKafkaAdminCommand(tenantID types.TenantID, envelope kafkaEnvelope, payload adminCommandPayload) (map[string]any, error) { eventType := strings.TrimSpace(envelope.EventType) if eventType == adminCommandUserPasswordReset { return executeKafkaUserPasswordResetCommand(tenantID, payload) } if eventType == adminCommandMessageHide || eventType == adminCommandMessageDelete { return executeKafkaMessageAdminCommand(tenantID, envelope, payload) } if eventType == adminCommandUserBan || eventType == adminCommandUserUnban || eventType == adminCommandUserFreeze || eventType == adminCommandUserUnfreeze || eventType == adminCommandUserDeviceKick || eventType == adminCommandUserLogoutAll || eventType == adminCommandUserCancel || eventType == adminCommandUserDelete { return executeKafkaUserGovernanceCommand(tenantID, envelope, payload, types.ZeroUid) } actor := parseKafkaCommandUser(payload.ActorUserID) if actor.IsZero() { return nil, types.ErrMalformed } switch eventType { case adminCommandKafkaDeadReplay: dead, err := replayKafkaDeadLetter(context.Background(), tenantID, payload.DeadID, actor, firstNonEmpty(payload.Note, payload.Reason)) if err != nil { return nil, err } return kafkaDeadLetterResult(dead), nil case adminCommandKafkaDeadIgnore: if err := resolveKafkaDeadLetter(tenantID, payload.DeadID, types.KafkaDeadStatusIgnored, actor, firstNonEmpty(payload.Note, payload.Reason)); err != nil { return nil, err } return map[string]any{"dead_id": payload.DeadID, "status": types.KafkaDeadStatusIgnored}, nil case adminCommandKafkaDeadResolve: if err := resolveKafkaDeadLetter(tenantID, payload.DeadID, types.KafkaDeadStatusResolved, actor, firstNonEmpty(payload.Note, payload.Reason)); err != nil { return nil, err } return map[string]any{"dead_id": payload.DeadID, "status": types.KafkaDeadStatusResolved}, nil } if store.GroupBusiness == nil { return nil, errors.New("group business store is not initialized") } topic := strings.TrimSpace(payload.Topic) if topic == "" { return nil, types.ErrMalformed } now := time.Now().UTC().Round(time.Millisecond) switch strings.TrimSpace(envelope.EventType) { case adminCommandGroupProfileUpdate: settings, err := store.GroupBusiness.UpdateSettings(tenantID, topic, types.GroupSettingsPatch{ Name: payload.Profile.Name, AvatarFileID: payload.Profile.AvatarFileID, Description: payload.Profile.Description, Version: payload.Version, }) if err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.settings.updated", map[string]any{"topic": topic, "version": settings.Version}) return map[string]any{"topic": topic, "version": settings.Version, "updated_at": settings.UpdatedAt}, nil case adminCommandGroupGovernUpdate: settings, err := store.GroupBusiness.UpdateSettings(tenantID, topic, types.GroupSettingsPatch{ JoinApproval: payload.Governance.JoinApproval, MuteAll: payload.Governance.MuteAll, MemberUploadFiles: payload.Governance.MemberUploadFiles, MemberPrivateChat: payload.Governance.MemberPrivateChat, MemberInvite: payload.Governance.MemberInvite, MaxMemberCount: payload.Governance.MaxMemberCount, MaxAdminCount: payload.Governance.MaxAdminCount, Version: payload.Version, }) if err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.settings.updated", map[string]any{"topic": topic, "version": settings.Version}) return map[string]any{"topic": topic, "version": settings.Version, "updated_at": settings.UpdatedAt}, nil case adminCommandGroupAdminSet: users := payload.UserIDs if len(users) == 0 && payload.UserID != "" { users = []string{payload.UserID} } if len(users) == 0 { return nil, types.ErrMalformed } succeeded := make([]map[string]any, 0, len(users)) for _, raw := range users { target := parseKafkaCommandUser(raw) if target.IsZero() { return map[string]any{"succeeded": succeeded}, types.ErrMalformed } if err := store.GroupBusiness.UpdateRole(tenantID, topic, actor, target, types.GroupMemberRoleAdmin, types.ModeCAdmin|types.ModeCPublic, now); err != nil { return map[string]any{"succeeded": succeeded, "failed_user_id": raw}, err } succeeded = append(succeeded, map[string]any{"user_id": internalAdminUIDRaw(target), "role": types.GroupMemberRoleAdmin}) } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.role.updated", map[string]any{"topic": topic, "succeeded": succeeded}) return map[string]any{"topic": topic, "succeeded": succeeded}, nil case adminCommandGroupAdminRemove: target := parseKafkaCommandUser(firstNonEmpty(payload.UserID, payload.TargetID)) if target.IsZero() { return nil, types.ErrMalformed } if err := store.GroupBusiness.UpdateRole(tenantID, topic, actor, target, types.GroupMemberRoleMember, types.ModeCPublic, now); err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.role.updated", map[string]any{"user_id": internalAdminUIDRaw(target), "role": types.GroupMemberRoleMember}) return map[string]any{"topic": topic, "user_id": internalAdminUIDRaw(target), "role": types.GroupMemberRoleMember}, nil case adminCommandGroupMemberMute: target := parseKafkaCommandUser(firstNonEmpty(payload.UserID, payload.TargetID)) if target.IsZero() { return nil, types.ErrMalformed } mute, err := store.GroupBusiness.UpsertMute(tenantID, &types.GroupMute{ Topic: topic, UserID: target, MutedBy: actor, MutedAt: now, ExpiresAt: payload.ExpiresAt, Reason: strings.TrimSpace(payload.Reason), Status: types.GroupMuteStatusActive, }) if err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.mute.updated", map[string]any{"user_id": internalAdminUIDRaw(target), "muted": true, "version": mute.Version}) return map[string]any{"topic": topic, "user_id": internalAdminUIDRaw(target), "muted": true, "version": mute.Version}, nil case adminCommandGroupMemberUnmute: target := parseKafkaCommandUser(firstNonEmpty(payload.UserID, payload.TargetID)) if target.IsZero() { return nil, types.ErrMalformed } if err := store.GroupBusiness.DeleteMute(tenantID, topic, target, actor, now); err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.mute.updated", map[string]any{"user_id": internalAdminUIDRaw(target), "muted": false}) return map[string]any{"topic": topic, "user_id": internalAdminUIDRaw(target), "muted": false}, nil case adminCommandGroupMemberRemove: target := parseKafkaCommandUser(firstNonEmpty(payload.UserID, payload.TargetID)) if target.IsZero() { return nil, types.ErrMalformed } if err := store.GroupBusiness.Leave(tenantID, topic, target, now); err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.member.left", map[string]any{"user_id": internalAdminUIDRaw(target)}) return map[string]any{"topic": topic, "user_id": internalAdminUIDRaw(target), "status": "left"}, nil case adminCommandGroupOwnerTransfer: target := parseKafkaCommandUser(firstNonEmpty(payload.TargetID, payload.UserID)) if target.IsZero() { return nil, types.ErrMalformed } if err := store.GroupBusiness.TransferOwner(tenantID, topic, actor, target, payload.Version, now); err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.owner.transferred", map[string]any{"owner": internalAdminUIDRaw(target), "previous_owner": internalAdminUIDRaw(actor)}) return map[string]any{"topic": topic, "owner": internalAdminUIDRaw(target), "previous_owner": internalAdminUIDRaw(actor)}, nil case adminCommandGroupDissolve: if err := dissolveGroupByAdminCommand(tenantID, topic, actor, strings.TrimSpace(payload.Reason), now); err != nil { return nil, err } publishIMGroupRealtimeEvent(tenantID, topic, actor, "group.dissolved", map[string]any{"topic": topic, "reason": strings.TrimSpace(payload.Reason)}) return map[string]any{"topic": topic, "status": "dissolved"}, nil } return nil, errors.New("unsupported admin command: " + envelope.EventType) } func dissolveGroupByAdminCommand(tenantID types.TenantID, topic string, actor types.Uid, reason string, now time.Time) error { if strings.TrimSpace(topic) == "" || actor.IsZero() { return types.ErrMalformed } if store.GroupBusiness == nil { return errors.New("group business store is not initialized") } _, err := store.GroupBusiness.Dissolve(tenantID, topic, actor, reason, now) return err } func executeKafkaMessageAdminCommand(tenantID types.TenantID, envelope kafkaEnvelope, payload adminCommandPayload) (map[string]any, error) { conversationType := strings.TrimSpace(payload.ConversationType) topic := strings.TrimSpace(payload.Topic) if (conversationType != "private" && conversationType != "group") || topic == "" || payload.SeqID <= 0 { return nil, types.ErrMalformed } action := strings.TrimPrefix(strings.TrimSpace(envelope.EventType), "message.") actorAdminID := int64(0) if envelope.Actor != nil { actorAdminID, _ = strconv.ParseInt(strings.TrimSpace(envelope.Actor.ID), 10, 64) } now := time.Now().UTC().Round(time.Millisecond) commandID := firstNonEmpty(payload.CommandID, envelope.EventID) actionID := "action_" + envelope.EventID if len(actionID) > 96 { actionID = actionID[:96] } requestID := strings.TrimSpace(envelope.TraceID) if len(requestID) > 64 { requestID = requestID[:64] } if err := store.SecurityAudit.ApplyMessageAction(tenantID, &types.MessageAdminAction{ ActionID: actionID, CommandID: commandID, ConversationType: conversationType, Topic: topic, SeqID: payload.SeqID, Action: action, Reason: strings.TrimSpace(payload.Reason), ActorAdminID: actorAdminID, RequestID: requestID, ProcessedAt: now, }); err != nil { return nil, err } if conversationType == "group" { if err := store.InvalidateHotMessages(tenantID, topic, []types.Range{{Low: payload.SeqID}}); err != nil { logs.Warn.Printf("message admin cache invalidation failed tenant:%d topic:%s seq:%d err:%v", tenantID, topic, payload.SeqID, err) } } return map[string]any{ "conversation_type": conversationType, "topic": topic, "seq_id": payload.SeqID, "action": action, "status": "success", "processed_at": now, }, nil } func executeKafkaUserGovernanceCommand(tenantID types.TenantID, envelope kafkaEnvelope, payload adminCommandPayload, actor types.Uid) (map[string]any, error) { target := parseKafkaCommandUser(firstNonEmpty(payload.TargetID, payload.UserID)) if target.IsZero() { return nil, types.ErrMalformed } eventType := strings.TrimSpace(envelope.EventType) now := time.Now().UTC().Round(time.Millisecond) result := map[string]any{ "user_id": internalAdminUIDRaw(target), "action": eventType, } switch eventType { case adminCommandUserBan, adminCommandUserFreeze: if err := store.Users.UpdateState(tenantID, target, types.StateSuspended); err != nil { return nil, err } evictUserSessions(tenantID, target) notifyUserStateChanged(tenantID, target, types.StateSuspended) result["state"] = types.StateSuspended.String() result["evicted"] = true return result, nil case adminCommandUserUnban, adminCommandUserUnfreeze: if err := store.Users.UpdateState(tenantID, target, types.StateOK); err != nil { return nil, err } notifyUserStateChanged(tenantID, target, types.StateOK) result["state"] = types.StateOK.String() return result, nil case adminCommandUserDeviceKick: deviceID := strings.TrimSpace(payload.DeviceID) if deviceID == "" { return nil, types.ErrMalformed } evicted := 0 if globals.sessionStore != nil { evicted = globals.sessionStore.EvictUserDevice(target, deviceID) } publishRealtimeEvictDevice(tenantID, target, deviceID, "admin_device_kick") if store.Devices != nil { if err := store.Devices.Delete(tenantID, target, deviceID); err != nil && !errors.Is(err, types.ErrNotFound) { return nil, err } } result["device_id"] = deviceID result["evicted_sessions"] = evicted return result, nil case adminCommandUserLogoutAll: evictUserSessions(tenantID, target) result["evicted"] = true return result, nil case adminCommandUserCancel, adminCommandUserDelete: hard := payload.Hard || eventType == adminCommandUserDelete if err := deleteUserByAdminCommand(tenantID, target, hard, now); err != nil { return nil, err } result["state"] = types.StateDeleted.String() result["hard"] = hard return result, nil } return nil, errors.New("unsupported user governance command: " + eventType) } func executeKafkaUserPasswordResetCommand(tenantID types.TenantID, payload adminCommandPayload) (map[string]any, error) { target := parseKafkaCommandUser(firstNonEmpty(payload.TargetID, payload.UserID)) if target.IsZero() || payload.PasswordHash == "" { return nil, types.ErrMalformed } passwordHash, err := base64.StdEncoding.DecodeString(payload.PasswordHash) if err != nil || len(passwordHash) == 0 { return nil, types.ErrMalformed } _, authLevel, _, _, err := store.Users.GetAuthRecord(tenantID, target) if err != nil { return nil, err } if err = store.Users.UpdateAuthRecord(tenantID, target, authLevel, "", passwordHash, time.Time{}); err != nil { return nil, err } now := time.Now().UTC().Round(time.Millisecond) authVersion := 0 if store.Security != nil { version, err := store.Security.BumpAuthVersion(tenantID, target, now) if err != nil { return nil, err } authVersion = version } evictUserSessions(tenantID, target) return map[string]any{ "user_id": internalAdminUIDRaw(target), "action": adminCommandUserPasswordReset, "auth_version": authVersion, "evicted": true, }, nil } func evictUserSessions(tenantID types.TenantID, uid types.Uid) { if globals.sessionStore != nil { globals.sessionStore.EvictUser(uid, "") } publishRealtimeEvictUser(tenantID, uid, "admin_user_evict", "") } func notifyUserStateChanged(tenantID types.TenantID, uid types.Uid, state types.ObjState) { usersRemoveUser(tenantID, uid) if globals.hub != nil && globals.hub.userStatus != nil { globals.hub.userStatus <- &userStatusReq{tenantID: tenantID, forUser: uid, state: state} } } func deleteUserByAdminCommand(tenantID types.TenantID, uid types.Uid, hard bool, _ time.Time) error { evictUserSessions(tenantID, uid) usersRemoveUser(tenantID, uid) if globals.hub != nil && globals.hub.unreg != nil { done := make(chan bool) globals.hub.unreg <- &topicUnreg{tenantID: tenantID, forUser: uid, del: hard, done: done} <-done } return store.Users.Delete(tenantID, uid, hard) } func publishKafkaAdminCommandResult(ctx context.Context, command kafkaEnvelope, commandID, status, reason, message string, result map[string]any) error { if commandID == "" { commandID = command.EventID } payload := adminCommandResultPayload{ CommandID: commandID, CommandEventID: command.EventID, Status: status, Reason: reason, Message: message, Result: result, } encoded, err := json.Marshal(payload) if err != nil { return err } eventID := stableIMEventID("admin-command-result", command.EventID, status, reason) return publishKafkaEnvelope(ctx, "admin_command_result", commandID, kafkaEnvelope{ EventID: eventID, TraceID: command.TraceID, TenantCode: command.TenantCode, SourceService: "im-server", TargetService: command.SourceService, EventType: "admin.command." + status, BizType: command.BizType, BizID: command.BizID, IdempotencyKey: command.IdempotencyKey, OccurredAt: time.Now().UTC(), ParentEventID: command.EventID, Payload: encoded, }) } func kafkaTenantID(tenantCode string) (types.TenantID, error) { tenant, err := store.Tenants.GetByCode(strings.TrimSpace(tenantCode)) if err != nil { return types.ZeroTenantID, err } if tenant == nil || tenant.ID.IsZero() { return types.ZeroTenantID, types.ErrNotFound } return tenant.ID, nil } func kafkaTenantIDFromEnvelope(envelope kafkaEnvelope) (types.TenantID, error) { if !envelope.TenantID.IsZero() { return envelope.TenantID, nil } return kafkaTenantID(envelope.TenantCode) } func markKafkaDeadLetter(ctx context.Context, tenantID types.TenantID, envelope kafkaEnvelope, consume *types.KafkaConsumeRecord, reason, message string) error { encoded, err := json.Marshal(envelope) if err != nil { return err } retryCount := 0 sourceTopic := kafkaTopic("admin_command") if consume != nil { retryCount = consume.RetryCount if strings.TrimSpace(consume.SourceTopic) != "" { sourceTopic = consume.SourceTopic } } deadID := stableIMEventID("dead", envelope.EventID, kafkaBus.config.ConsumerGroup, retryCount) dead := &types.KafkaDeadLetter{ DeadID: deadID, EventID: envelope.EventID, ParentEventID: envelope.ParentEventID, SourceTopic: sourceTopic, ConsumerGroup: kafkaBus.config.ConsumerGroup, EventType: envelope.EventType, BizType: envelope.BizType, BizID: envelope.BizID, Envelope: encoded, RetryCount: retryCount, LastReason: reason, LastError: message, Status: types.KafkaDeadStatusDead, } now := time.Now().UTC().Round(time.Millisecond) deadPayload, err := json.Marshal(dead) if err != nil { return err } return publishKafkaDeadLetter(ctx, "dead_letter", envelope.EventID, kafkaEnvelope{ EventID: deadID, TraceID: envelope.TraceID, TenantCode: envelope.TenantCode, TenantID: tenantID, SourceService: "im-server", TargetService: "hotgo-admin", EventType: "kafka.dead_letter.created", BizType: envelope.BizType, BizID: envelope.BizID, OccurredAt: now, ParentEventID: envelope.EventID, Payload: deadPayload, }) } func shouldDeadLetterKafkaCommand(reason string, retryCount int) bool { if !kafkaCommandReasonRetryable(reason) { return true } if kafkaBus == nil { return true } return retryCount >= kafkaBus.config.MaxRetryCount } func kafkaCommandReasonRetryable(reason string) bool { switch reason { case "COMMAND_FAILED": return true default: return false } } func sleepKafkaRetryBackoff(retryCount int) { if kafkaBus == nil || retryCount <= 0 { return } backoffs := kafkaBus.config.RetryBackoffMS index := retryCount - 1 if index >= len(backoffs) { index = len(backoffs) - 1 } delay := time.Duration(backoffs[index]) * time.Millisecond if delay > 0 { time.Sleep(delay) } } func parseKafkaCommandUser(raw string) types.Uid { raw = strings.TrimSpace(raw) if raw == "" { return types.ZeroUid } if id, err := strconv.ParseInt(raw, 10, 64); err == nil && id > 0 { return store.EncodeUid(id) } uid := types.ParseUserId(raw) if !uid.IsZero() { return uid } return types.ParseUid(raw) } func kafkaDeadLetterResult(dead *types.KafkaDeadLetter) map[string]any { if dead == nil { return nil } return map[string]any{ "dead_id": dead.DeadID, "event_id": dead.EventID, "source_topic": dead.SourceTopic, "event_type": dead.EventType, "biz_type": dead.BizType, "biz_id": dead.BizID, "status": dead.Status, "replay_count": dead.ReplayCount, "updated_at": dead.UpdatedAt, } } func kafkaCommandErrorReason(err error) string { if err == nil { return "" } switch { case errors.Is(err, types.ErrMalformed): return "MALFORMED_COMMAND" case errors.Is(err, types.ErrPolicy): return "POLICY_VIOLATION" case errors.Is(err, types.ErrNotFound): return "RESOURCE_NOT_FOUND" case errors.Is(err, types.ErrPermissionDenied): return "FORBIDDEN" case errors.Is(err, types.ErrDuplicate): return "COMMAND_DUPLICATED" case errors.Is(err, types.ErrInvalidResponse): return "GROUP_VERSION_CONFLICT" default: if strings.Contains(err.Error(), "unsupported admin command") { return "UNSUPPORTED_COMMAND" } return "COMMAND_FAILED" } } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" }