use queue::Labels; use sea_orm::DbErr; use serde_json::Value; use crate::label; macro_rules! event_kinds { ($($variant:ident: $kind:literal @ $topic:literal => $tag:literal;)*) => { /// 业务事件的唯一真源:wire kind、topic area、tag、durable 与串行化粒度都从这里派生。 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum EventKind { $($variant,)* } impl EventKind { pub const ALL: &'static [EventKind] = &[$(EventKind::$variant,)*]; pub const fn as_str(self) -> &'static str { match self { $(Self::$variant => $kind,)* } } pub fn parse(kind: &str) -> Option { match kind { $($kind => Some(Self::$variant),)* _ => None, } } /// RocketMQ topic 的 area 段:实际 topic 是 `-`。 pub const fn topic(self) -> &'static str { match self { $(Self::$variant => $topic,)* } } fn only_tag(self) -> &'static [&'static str] { match self { $(Self::$variant => &[$tag],)* } } } }; } event_kinds! { AuthRegisterCreate: "auth.register.create" @ "auth" => "register"; AuthLoginCreate: "auth.login.create" @ "auth" => "login"; AuthLogoutDelete: "auth.logout.delete" @ "auth" => "logout"; AuthVerificationTokenCreate: "auth.verification_token.create" @ "auth" => "send-code"; AuthPasswordCreate: "auth.password.create" @ "auth" => "request-reset"; AuthPasswordUpdate: "auth.password.update" @ "auth" => "reset-password"; AuthSshKeyCreate: "auth.ssh_key.create" @ "auth" => "add-sshkey"; AuthSshKeyDelete: "auth.ssh_key.delete" @ "auth" => "delete-sshkey"; AuthAccessKeyCreate: "auth.access_key.create" @ "auth" => "create-accesskey"; AuthAccessKeyDelete: "auth.access_key.delete" @ "auth" => "delete-accesskey"; AuthAccessKeyUsed: "auth.access_key.used" @ "auth" => "use-accesskey"; AuthAppTokenCreate: "auth.app_token.create" @ "auth" => "issue-token"; AuthTokenCreate: "auth.token.create" @ "auth" => "issue-token"; AuthChallengeCreate: "auth.challenge.create" @ "auth" => "challenge"; AuthChallengeUsed: "auth.challenge.used" @ "auth" => "verify-challenge"; EmailDeliveryCreate: "email.delivery.create" @ "auth" => "delivery"; EmailFailureCreate: "email.failure.create" @ "auth" => "failure"; OrganizationLifeCycleCreate: "organization.life_cycle.create" @ "org" => "create"; OrganizationLifeCycleUpdate: "organization.life_cycle.update" @ "org" => "update"; OrganizationLifeCycleDelete: "organization.life_cycle.delete" @ "org" => "delete"; OrganizationInviteCreate: "organization.invite.create" @ "org" => "invite"; OrganizationMemberUpdate: "organization.member.update" @ "org" => "update-member"; OrganizationRoleUpdate: "organization.role.update" @ "org" => "grant"; ChatMessageCreate: "chat.message.create" @ "chat" => "message-create"; ChatMessageUpdate: "chat.message.update" @ "chat" => "message-update"; ChatMessageDelete: "chat.message.delete" @ "chat" => "message-delete"; ChatMessagesDelete: "chat.messages.delete" @ "chat" => "messages-delete"; ChatPinCreate: "chat.pin.create" @ "chat" => "pin-create"; ChatPinDelete: "chat.pin.delete" @ "chat" => "pin-delete"; ChatReactionUpdate: "chat.reaction.update" @ "chat" => "reaction-update"; ChatTypingCreate: "chat.typing.create" @ "chat" => "typing-create"; ChatRoomUpdate: "chat.room.update" @ "chat" => "room-update"; ChatMemberCreate: "chat.member.create" @ "chat" => "member-create"; ChatMemberDelete: "chat.member.delete" @ "chat" => "member-delete"; ChatThreadUpdate: "chat.thread.update" @ "chat" => "thread-update"; ChatVoicePresence: "chat.voice.presence" @ "chat" => "voice-presence"; ChatVoiceHand: "chat.voice.hand" @ "chat" => "voice-hand"; ChatVoiceLayout: "chat.voice.layout" @ "chat" => "voice-layout"; ChatVoiceModerate: "chat.voice.moderate" @ "chat" => "voice-moderate"; ChatMentionCreate: "chat.mention.create" @ "chat" => "mention-create"; ChatReminderCreate: "chat.reminder.create" @ "chat" => "reminder-create"; ChatMembershipCreate: "chat.membership.create" @ "chat" => "membership-create"; ChatReadStateUpdate: "chat.read_state.update" @ "chat" => "read_state-update"; ChatPresenceUpdate: "chat.presence.update" @ "chat" => "presence-update"; NotifyNotificationCreate: "notify.notification.create" @ "notify" => "notification-create"; IssueIssueCreate: "issue.issue.create" @ "issue" => "issue-create"; IssueTitleUpdate: "issue.title.update" @ "issue" => "title-update"; IssueDescriptionUpdate: "issue.description.update" @ "issue" => "description-update"; IssueContentUpdate: "issue.content.update" @ "issue" => "content-update"; IssueMilestoneUpdate: "issue.milestone.update" @ "issue" => "milestone-update"; IssuePriorityUpdate: "issue.priority.update" @ "issue" => "priority-update"; IssueDueUpdate: "issue.due.update" @ "issue" => "due-update"; IssueIssueClose: "issue.issue.close" @ "issue" => "issue-close"; IssueIssueReopen: "issue.issue.reopen" @ "issue" => "issue-reopen"; IssueCommentCreate: "issue.comment.create" @ "issue" => "comment-create"; IssueCommentUpdate: "issue.comment.update" @ "issue" => "comment-update"; IssueCommentDelete: "issue.comment.delete" @ "issue" => "comment-delete"; IssueMemberCreate: "issue.member.create" @ "issue" => "member-create"; IssueMemberDelete: "issue.member.delete" @ "issue" => "member-delete"; IssueLabelCreate: "issue.label.create" @ "issue" => "label-create"; IssueLabelDelete: "issue.label.delete" @ "issue" => "label-delete"; IssueDependencyCreate: "issue.dependency.create" @ "issue" => "dependency-create"; IssueDependencyDelete: "issue.dependency.delete" @ "issue" => "dependency-delete"; IssuePinCreate: "issue.pin.create" @ "issue" => "pin-create"; IssuePinDelete: "issue.pin.delete" @ "issue" => "pin-delete"; IssueRepoRelationCreate: "issue.repo-relation.create" @ "issue" => "repo-relation-create"; IssueRepoRelationDelete: "issue.repo-relation.delete" @ "issue" => "repo-relation-delete"; IssuePullLinkCreate: "issue.pull-link.create" @ "issue" => "pull-link-create"; IssuePullLinkDelete: "issue.pull-link.delete" @ "issue" => "pull-link-delete"; PullPullCreate: "pull.pull.create" @ "pull" => "pull-create"; PullPullUpdate: "pull.pull.update" @ "pull" => "pull-update"; PullPullClose: "pull.pull.close" @ "pull" => "pull-close"; PullPullReopen: "pull.pull.reopen" @ "pull" => "pull-reopen"; PullPullMerge: "pull.pull.merge" @ "pull" => "pull-merge"; PullCommentCreate: "pull.comment.create" @ "pull" => "comment-create"; PullReviewCreate: "pull.review.create" @ "pull" => "review-create"; PullReviewDelete: "pull.review.delete" @ "pull" => "review-delete"; PullReviewRequestCreate: "pull.review-request.create" @ "pull" => "review-request-create"; PullReviewRequestDelete: "pull.review-request.delete" @ "pull" => "review-request-delete"; PullHeadUpdate: "pull.head.update" @ "pull" => "head-update"; PullLabelCreate: "pull.label.create" @ "pull" => "label-create"; PullLabelDelete: "pull.label.delete" @ "pull" => "label-delete"; PullAssigneeCreate: "pull.assignee.create" @ "pull" => "assignee-create"; PullAssigneeDelete: "pull.assignee.delete" @ "pull" => "assignee-delete"; PullLockCreate: "pull.lock.create" @ "pull" => "lock-create"; PullLockDelete: "pull.lock.delete" @ "pull" => "lock-delete"; PullAutoMergeCreate: "pull.auto-merge.create" @ "pull" => "auto-merge-create"; PullAutoMergeDelete: "pull.auto-merge.delete" @ "pull" => "auto-merge-delete"; } const ORGANIZATION_MEMBER_TAGS: &[&str] = &[ "update-member", "join", "leave", "add-member", "remove-member", "set-role", "ban", "unban", "update-ban", "transfer-owner", ]; const ORGANIZATION_ROLE_TAGS: &[&str] = &[ "grant", "create-role", "set-permissions", "update-role", "delete-role", "assign-role", "unassign-role", ]; impl EventKind { /// wire kind 的域名段,也是 envelope 的 `source`。 pub fn domain(self) -> &'static str { self.as_str().split('.').next().unwrap_or_default() } /// 该 kind 可能出现的全部 tag,首个是默认值;订阅表达式由此生成。 pub fn tags(self) -> &'static [&'static str] { match self { Self::AuthRegisterCreate => &["register", "send-code"], Self::OrganizationLifeCycleUpdate => &["update", "archive", "restore"], Self::OrganizationInviteCreate => &["invite", "revoke-invite", "redeem-invite"], Self::OrganizationMemberUpdate => ORGANIZATION_MEMBER_TAGS, Self::OrganizationRoleUpdate => ORGANIZATION_ROLE_TAGS, _ => self.only_tag(), } } /// 实际投递用的 tag:多数 kind 与其他 tag 无关,少数按 payload 细分。 pub fn tag(self, payload: Option<&Value>) -> &'static str { match self { Self::AuthRegisterCreate if has_field(payload, "expires_in_secs") => "send-code", Self::OrganizationLifeCycleUpdate => match action(payload) { Some("archive") => "archive", Some("restore") => "restore", _ => "update", }, Self::OrganizationInviteCreate => match action(payload) { Some("revoke") => "revoke-invite", Some("redeem") => "redeem-invite", _ => "invite", }, Self::OrganizationMemberUpdate => match action(payload) { Some("join_role") => "join", Some("leave_role") => "leave", Some("add") => "add-member", Some("remove") => "remove-member", Some("set_role") => "set-role", Some("ban") => "ban", Some("unban") => "unban", Some("update_ban") => "update-ban", Some("transfer_ownership") => "transfer-owner", _ => "update-member", }, Self::OrganizationRoleUpdate => match action(payload) { Some("create") => "create-role", Some("set_permissions") => "set-permissions", Some("update") => "update-role", Some("delete") => "delete-role", Some("assign") => "assign-role", Some("unassign") => "unassign-role", _ => "grant", }, other => other.tags()[0], } } /// 是否允许写入 outbox:只有持久业务域的事件才进 MQ。 pub fn durable(self) -> bool { matches!( self.topic(), "auth" | "org" | "chat" | "notify" | "issue" | "pull" ) } /// 身份域事件走身份 publisher(独立的连接与消费组)。 pub fn identity(self) -> bool { matches!(self.domain(), "auth" | "email" | "organization") } /// 只投递给单个用户的个人事件族(不走资源广播)。 pub const fn personal(self) -> bool { matches!( self, Self::NotifyNotificationCreate | Self::ChatMentionCreate | Self::ChatReminderCreate | Self::ChatMembershipCreate | Self::ChatReadStateUpdate | Self::ChatPresenceUpdate ) } /// 串行化粒度:同一 `ordering_key` 的事件严格按写入顺序投递。 pub fn ordering_key(self, labels: &Labels) -> Result { if let Some(explicit) = labels .get(label::ORDERING_KEY) .filter(|key| !key.is_empty()) { return Ok(explicit.to_string()); } let domain = self.domain(); let key = match domain { "chat" => { if let Some(uid) = labels.get(label::RECIPIENT_USER_ID) { return Ok(format!("chat:user:{uid}")); } if let Some(room) = labels.get(label::ROOM_ID) { return Ok(format!("chat:room:{room}")); } labels.get(label::USER_ID) } "notify" => { if let Some(uid) = labels .get(label::RECIPIENT_USER_ID) .or_else(|| labels.get(label::USER_ID)) { return Ok(format!("notify:user:{uid}")); } None } "pull" => labels .get("pull_id") .or_else(|| labels.get(label::PULL_REQUEST_ID)) .or_else(|| labels.get(label::REPOSITORY_ID)), "issue" => labels .get(label::ISSUE_ID) .or_else(|| labels.get(label::BOARD_ID)) .or_else(|| labels.get(label::PULL_REQUEST_ID)) .or_else(|| labels.get(label::GUILD_ID)), "organization" => labels.get(label::ORGANIZATION_ID), "auth" => labels .get(label::USER_ID) .or_else(|| labels.get(label::REQUEST_ID)), "email" => labels.get(label::REQUEST_ID), _ => None, }; key.filter(|key| !key.is_empty()) .map(|key| format!("{domain}:{key}")) .ok_or_else(|| { DbErr::Custom(format!( "event `{}` requires a user, organization or request id label", self.as_str() )) }) } } impl std::fmt::Display for EventKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } } fn action(payload: Option<&Value>) -> Option<&str> { payload .and_then(|value| value.get("action")) .and_then(Value::as_str) } fn has_field(payload: Option<&Value>, key: &str) -> bool { payload.is_some_and(|value| value.get(key).is_some_and(|value| !value.is_null())) }