Skip to content
Clone

feat(events): add the realtime gateway and internal rpc handlers

- move the RocketMQ consumer pipeline, action fan-out, subscription registry
  and delivery ordering into an optional `gateway` feature on `events`, so
  business crates that only emit outbox events do not pull in the socket stack
- deliver `gitdata-auth` events straight to the target user and fan
  `gitdata-org` events out to space members, resolving membership through the
  internal RPC client instead of an external auth service
- expose `EventIngressService` (publish/revoke) and `SubscriptionService`
  (subscribe/unsubscribe/list) with handlers mounted on `EventService`; both
  the socket path and the RPC path share the same subscribe and authorize code
- keep the subscription whitelist machine-readable under `lib/events/data`
  and drop the retired auth-service source list
ZhenYi committed
parent 273bd1b commit 8dece69 Browse files

20 files changed

+6084 −1

File tree 20
Added lib/events/data/events.json View lib/events/data/events.json
@@ -0,0 +1,3390 @@
1{
2 "generated_at": "2026-10-30",
3 "sources": {
4 "kenshell": {
5 "path": "../KenShell",
6 "version": "1.0.0",
7 "producer_group": "kenshell",
8 "topic_prefix": "kenshell",
9 "event_definition": "src/events/event.rs",
10 "emit": "src/svc/mod.rs:19 (AppSvc::emit)",
11 "publish": "src/events/outbox.rs (durable outbox + relay)"
12 },
13 "kenapi": {
14 "path": "../KenApi",
15 "version": "1.0.0",
16 "producer_group": "gitdata",
17 "topic_prefix": "gitdata",
18 "event_definition": "lib/chat/src/share/events.rs (room_kind / user_kind), lib/export/src/notify.rs (notification)",
19 "routing": "lib/queue/src/route.rs",
20 "emit": "lib/export/src/events.rs::enqueue <- lib/chat/src/share/realtime.rs::publish_room/publish_user (post-commit), lib/export/src/notify.rs::publish_user_event (post-commit), lib/issues/src/services/service.rs::write_event (same connection as timeline row), lib/pulls/src/services/events.rs::write_event (same connection as timeline row)",
21 "publish": "lib/queue/src/relay.rs (durable outbox + relay, max 50 attempts)",
22 "ordering_key": {
23 "chat.room": "chat:room:{room_id}",
24 "chat.user": "chat:user:{user_id}",
25 "notify": "notify:user:{user_id}",
26 "issue": "issue:{issue_id}",
27 "pull": "pull:{pull_request_id}"
28 },
29 "atomicity": {
30 "chat": "post-commit",
31 "notify": "post-commit",
32 "issue": "same connection as issue_event_line (transactional when caller passes a txn)",
33 "pull": "same connection as pull_request_event_line (transactional when caller passes a txn)"
34 },
35 "enabled": "config [rocketmq] enabled (default false); when false nothing is enqueued"
36 }
37 },
38 "message_model": {
39 "topic": "<topic_prefix>-<area>",
40 "body_fields": [
41 "id",
42 "kind",
43 "occurred_at",
44 "summary"
45 ],
46 "summary_equals_tag": true,
47 "payload_on_wire": false,
48 "keys": "envelope id (UUID v7)"
49 },
50 "notes": [
51 "三个项目都不把业务 payload 写进消息体;payload 只用于决定 tag 和派生消息属性。",
52 "KenOAuth 自 2026-10-30 起走 outbox 投递:组织域在业务事务内入队,其余域提交后入队。",
53 "KenOAuth 组织写路径(26 处)写不进 outbox 即让业务事务回滚,与 KenShell 同级;auth/email 域写库失败时退回直接投递,50 次重试后丢弃。",
54 "KenOAuth 已投递行保留 7 天;KenShell 的 event_outbox 已投递行不清理。",
55 "properties 中 user/org/repo_id/repo_name 都是条件出现的,request_id 在两个项目里都存在(KenOAuth 的 email 事件亦同)。",
56 "KenOAuth 的 route 未映射 organization.member.update 的 action=leave,实际 tag 为 update-member。",
57 "KenApi 默认 rocketmq.enabled=false,此时不写 outbox、relay 不启动,Redis pub/sub 与推送照常工作。",
58 "KenApi 的聊天与通知事件是提交后入队;议题/PR 事件与 timeline 行写在同一个连接上,调用方传事务时同事务提交。",
59 "KenApi 只投递本地事件:出站 webhook、应用推送、邮件、LiveKit、KenShell git RPC 属于对外部 RPC 的调用,不产生 MQ 事件。",
60 "KenApi 的 route 要求 kind 恰好三段,tag 恒为 name-operate,没有 KenOAuth 那种别名映射;chat.typing.create 与 10 个 issue / 2 个 pull 的 kind 已定义但无调用点。"
61 ],
62 "events": [
63 {
64 "project": "kenoauth",
65 "topic": "oauth-login",
66 "area": "login",
67 "tag": "login",
68 "kind": "auth.login.create",
69 "payload": {
70 "user_id": "i64",
71 "username": "string",
72 "mfa_required": "bool"
73 },
74 "labels": [
75 "user_id",
76 "request_id"
77 ],
78 "properties": [
79 "user",
80 "request_id"
81 ],
82 "source": "src/auth/login/service.rs:148,274,341",
83 "atomicity": "post-commit"
84 },
85 {
86 "project": "kenoauth",
87 "topic": "oauth-logout",
88 "area": "logout",
89 "tag": "logout",
90 "kind": "auth.logout.delete",
91 "payload": {
92 "user_id": "i64|null"
93 },
94 "labels": [
95 "user_id (optional)",
96 "request_id"
97 ],
98 "properties": [
99 "user",
100 "request_id"
101 ],
102 "source": "src/auth/logout/service.rs:75",
103 "atomicity": "post-commit"
104 },
105 {
106 "project": "kenoauth",
107 "topic": "oauth-register",
108 "area": "register",
109 "tag": "send-code",
110 "kind": "auth.register.create",
111 "payload": {
112 "expires_in_secs": "u64"
113 },
114 "labels": [
115 "request_id"
116 ],
117 "properties": [
118 "request_id"
119 ],
120 "source": "src/auth/register/service.rs:82",
121 "atomicity": "post-commit"
122 },
123 {
124 "project": "kenoauth",
125 "topic": "oauth-register",
126 "area": "register",
127 "tag": "register",
128 "kind": "auth.register.create",
129 "payload": {
130 "user_id": "i64",
131 "username": "string",
132 "displayname": "string|null"
133 },
134 "labels": [
135 "user_id",
136 "request_id"
137 ],
138 "properties": [
139 "user",
140 "request_id"
141 ],
142 "source": "src/auth/register/service.rs:206",
143 "atomicity": "post-commit"
144 },
145 {
146 "project": "kenoauth",
147 "topic": "oauth-verification",
148 "area": "verification",
149 "tag": "send-code",
150 "kind": "auth.verification_token.create",
151 "payload": {
152 "expires_in_secs": "u64"
153 },
154 "labels": [
155 "user_id",
156 "request_id"
157 ],
158 "properties": [
159 "user",
160 "request_id"
161 ],
162 "source": "src/auth/login/service.rs:227",
163 "atomicity": "post-commit"
164 },
165 {
166 "project": "kenoauth",
167 "topic": "oauth-password",
168 "area": "password",
169 "tag": "request-reset",
170 "kind": "auth.password.create",
171 "payload": {
172 "expires_in_secs": "u64"
173 },
174 "labels": [
175 "user_id",
176 "request_id"
177 ],
178 "properties": [
179 "user",
180 "request_id"
181 ],
182 "source": "src/auth/password_reset/service.rs:127",
183 "atomicity": "post-commit"
184 },
185 {
186 "project": "kenoauth",
187 "topic": "oauth-password",
188 "area": "password",
189 "tag": "reset-password",
190 "kind": "auth.password.update",
191 "payload": {
192 "user_id": "i64"
193 },
194 "labels": [
195 "user_id",
196 "request_id"
197 ],
198 "properties": [
199 "user",
200 "request_id"
201 ],
202 "source": "src/auth/password_reset/service.rs:249",
203 "atomicity": "post-commit"
204 },
205 {
206 "project": "kenoauth",
207 "topic": "oauth-sshkey",
208 "area": "sshkey",
209 "tag": "add-sshkey",
210 "kind": "auth.ssh_key.create",
211 "payload": {
212 "id": "uuid",
213 "name": "string",
214 "description": "string|null",
215 "algorithm": "enum",
216 "fingerprint": "string",
217 "public_key": "string",
218 "created_at": "datetime",
219 "expires_at": "datetime|null",
220 "last_used_at": "datetime|null",
221 "revoked_at": "datetime|null"
222 },
223 "labels": [
224 "user_id",
225 "request_id"
226 ],
227 "properties": [
228 "user",
229 "request_id"
230 ],
231 "source": "src/auth/ssh_key/service.rs:174",
232 "atomicity": "post-commit"
233 },
234 {
235 "project": "kenoauth",
236 "topic": "oauth-sshkey",
237 "area": "sshkey",
238 "tag": "delete-sshkey",
239 "kind": "auth.ssh_key.delete",
240 "payload": {
241 "user_id": "i64",
242 "ssh_key_id": "uuid"
243 },
244 "labels": [
245 "user_id",
246 "request_id"
247 ],
248 "properties": [
249 "user",
250 "request_id"
251 ],
252 "source": "src/auth/ssh_key/service.rs:279",
253 "atomicity": "post-commit"
254 },
255 {
256 "project": "kenoauth",
257 "topic": "oauth-accesskey",
258 "area": "accesskey",
259 "tag": "create-accesskey",
260 "kind": "auth.access_key.create",
261 "payload": {
262 "id": "uuid",
263 "name": "string",
264 "scope": "string",
265 "created_at": "datetime",
266 "expires_at": "datetime|null"
267 },
268 "labels": [
269 "user_id",
270 "request_id"
271 ],
272 "properties": [
273 "user",
274 "request_id"
275 ],
276 "source": "src/auth/access_token/service.rs:211",
277 "atomicity": "post-commit"
278 },
279 {
280 "project": "kenoauth",
281 "topic": "oauth-accesskey",
282 "area": "accesskey",
283 "tag": "rotate-accesskey",
284 "kind": "auth.access_key.create",
285 "payload": {
286 "id": "uuid",
287 "rotated": "bool"
288 },
289 "labels": [
290 "user_id",
291 "request_id"
292 ],
293 "properties": [
294 "user",
295 "request_id"
296 ],
297 "source": "src/auth/access_token/service.rs:301",
298 "atomicity": "post-commit"
299 },
300 {
301 "project": "kenoauth",
302 "topic": "oauth-accesskey",
303 "area": "accesskey",
304 "tag": "delete-accesskey",
305 "kind": "auth.access_key.delete",
306 "payload": {
307 "user_id": "i64",
308 "token_id": "uuid"
309 },
310 "labels": [
311 "user_id",
312 "request_id"
313 ],
314 "properties": [
315 "user",
316 "request_id"
317 ],
318 "source": "src/auth/access_token/service.rs:395",
319 "atomicity": "post-commit"
320 },
321 {
322 "project": "kenoauth",
323 "topic": "oauth-accesskey",
324 "area": "accesskey",
325 "tag": "use-accesskey",
326 "kind": "auth.access_key.used",
327 "payload": {
328 "token_id": "uuid",
329 "user_id": "i64",
330 "username": "string",
331 "scope": "string"
332 },
333 "labels": [
334 "user_id",
335 "request_id"
336 ],
337 "properties": [
338 "user",
339 "request_id"
340 ],
341 "source": "src/auth/access_token/service.rs:471",
342 "atomicity": "post-commit"
343 },
344 {
345 "project": "kenoauth",
346 "topic": "oauth-token",
347 "area": "token",
348 "tag": "issue-token",
349 "kind": "auth.app_token.create",
350 "payload": {
351 "app_id": "string",
352 "audience": "string",
353 "key_id": "string",
354 "issued_at": "string",
355 "expires_at": "string"
356 },
357 "labels": [
358 "user_id"
359 ],
360 "properties": [
361 "user"
362 ],
363 "source": "src/auth/app_token/service.rs:45",
364 "atomicity": "post-commit"
365 },
366 {
367 "project": "kenoauth",
368 "topic": "oauth-token",
369 "area": "token",
370 "tag": "issue-token",
371 "kind": "auth.app_token.create",
372 "payload": {
373 "audience": "string",
374 "kid": "string",
375 "issued_at": "string",
376 "expires_at": "string"
377 },
378 "labels": [
379 "user_id"
380 ],
381 "properties": [
382 "user"
383 ],
384 "source": "src/auth/token/service.rs:50",
385 "atomicity": "post-commit"
386 },
387 {
388 "project": "kenoauth",
389 "topic": "oauth-challenge",
390 "area": "challenge",
391 "tag": "challenge",
392 "kind": "auth.challenge.create",
393 "payload": {
394 "context": "string",
395 "difficulty": "u32",
396 "issued_at": "u64",
397 "expires_at": "u64"
398 },
399 "labels": [
400 "user_id (optional)",
401 "request_id"
402 ],
403 "properties": [
404 "user",
405 "request_id"
406 ],
407 "source": "src/auth/challenge/service.rs:17",
408 "atomicity": "post-commit"
409 },
410 {
411 "project": "kenoauth",
412 "topic": "oauth-challenge",
413 "area": "challenge",
414 "tag": "verify-challenge",
415 "kind": "auth.challenge.used",
416 "payload": {
417 "user_id": "i64|null"
418 },
419 "labels": [
420 "user_id (optional)",
421 "request_id"
422 ],
423 "properties": [
424 "user",
425 "request_id"
426 ],
427 "source": "src/auth/challenge/service.rs:40",
428 "atomicity": "post-commit"
429 },
430 {
431 "project": "kenoauth",
432 "topic": "oauth-email",
433 "area": "email",
434 "tag": "delivery",
435 "kind": "email.delivery.create",
436 "payload": {
437 "request_id": "uuid",
438 "recipient_count": "usize",
439 "smtp_message_id": "string",
440 "sent_at_ms": "i64",
441 "metadata": "map<string,string>"
442 },
443 "labels": [
444 "request_id"
445 ],
446 "properties": [
447 "request_id"
448 ],
449 "source": "src/email/service.rs:270,381,398",
450 "atomicity": "post-commit"
451 },
452 {
453 "project": "kenoauth",
454 "topic": "oauth-email",
455 "area": "email",
456 "tag": "failure",
457 "kind": "email.failure.create",
458 "payload": {
459 "request_id": "uuid",
460 "error": "string",
461 "recipient_count": "usize",
462 "failed_at_ms": "i64",
463 "metadata": "map<string,string>"
464 },
465 "labels": [
466 "request_id"
467 ],
468 "properties": [
469 "request_id"
470 ],
471 "source": "src/email/service.rs:295,433",
472 "atomicity": "post-commit"
473 },
474 {
475 "project": "kenoauth",
476 "topic": "oauth-org",
477 "area": "org",
478 "tag": "create",
479 "kind": "organization.life_cycle.create",
480 "payload": {
481 "id": "uuid",
482 "slug": "string",
483 "name": "string",
484 "request_id": "string|null"
485 },
486 "labels": [
487 "organization_id",
488 "actor_user_id"
489 ],
490 "properties": [
491 "user",
492 "org",
493 "request_id"
494 ],
495 "source": "src/auth/organization/service.rs:225",
496 "atomicity": "transactional"
497 },
498 {
499 "project": "kenoauth",
500 "topic": "oauth-org",
501 "area": "org",
502 "tag": "update",
503 "kind": "organization.life_cycle.update",
504 "payload": {
505 "id": "uuid",
506 "name": "string",
507 "request_id": "string|null"
508 },
509 "labels": [
510 "organization_id",
511 "actor_user_id"
512 ],
513 "properties": [
514 "user",
515 "org",
516 "request_id"
517 ],
518 "source": "src/auth/organization/service.rs:447",
519 "atomicity": "transactional"
520 },
521 {
522 "project": "kenoauth",
523 "topic": "oauth-org",
524 "area": "org",
525 "tag": "archive",
526 "kind": "organization.life_cycle.update",
527 "payload": {
528 "id": "uuid",
529 "action": "archive",
530 "request_id": "string|null"
531 },
532 "labels": [
533 "organization_id",
534 "actor_user_id"
535 ],
536 "properties": [
537 "user",
538 "org",
539 "request_id"
540 ],
541 "source": "src/auth/organization/service.rs:498",
542 "atomicity": "transactional"
543 },
544 {
545 "project": "kenoauth",
546 "topic": "oauth-org",
547 "area": "org",
548 "tag": "restore",
549 "kind": "organization.life_cycle.update",
550 "payload": {
551 "id": "uuid",
552 "action": "restore",
553 "request_id": "string|null"
554 },
555 "labels": [
556 "organization_id",
557 "actor_user_id"
558 ],
559 "properties": [
560 "user",
561 "org",
562 "request_id"
563 ],
564 "source": "src/auth/organization/service.rs:558",
565 "atomicity": "transactional"
566 },
567 {
568 "project": "kenoauth",
569 "topic": "oauth-org",
570 "area": "org",
571 "tag": "delete",
572 "kind": "organization.life_cycle.delete",
573 "payload": {
574 "id": "uuid",
575 "force": "bool",
576 "reason": "string|null",
577 "request_id": "string|null"
578 },
579 "labels": [
580 "organization_id",
581 "actor_user_id"
582 ],
583 "properties": [
584 "user",
585 "org",
586 "request_id"
587 ],
588 "source": "src/auth/organization/service.rs:681",
589 "atomicity": "transactional"
590 },
591 {
592 "project": "kenoauth",
593 "topic": "oauth-org",
594 "area": "org",
595 "tag": "transfer-owner",
596 "kind": "organization.member.update",
597 "payload": {
598 "id": "uuid",
599 "action": "transfer_ownership",
600 "previous_owner": "i64",
601 "new_owner": "i64",
602 "request_id": "string|null"
603 },
604 "labels": [
605 "organization_id",
606 "actor_user_id"
607 ],
608 "properties": [
609 "user",
610 "org",
611 "request_id"
612 ],
613 "source": "src/auth/organization/service.rs:760",
614 "atomicity": "transactional"
615 },
616 {
617 "project": "kenoauth",
618 "topic": "oauth-org",
619 "area": "org",
620 "tag": "invite",
621 "kind": "organization.invite.create",
622 "payload": {
623 "id": "uuid",
624 "method": "invite_link|invite_targeted|invite_email|direct",
625 "target_user_id": "i64|null",
626 "max_uses": "u32|null",
627 "request_id": "string|null"
628 },
629 "labels": [
630 "organization_id",
631 "actor_user_id"
632 ],
633 "properties": [
634 "user",
635 "org",
636 "request_id"
637 ],
638 "source": "src/auth/organization/invite.rs:157",
639 "atomicity": "transactional"
640 },
641 {
642 "project": "kenoauth",
643 "topic": "oauth-org",
644 "area": "org",
645 "tag": "revoke-invite",
646 "kind": "organization.invite.create",
647 "payload": {
648 "id": "uuid",
649 "action": "revoke",
650 "reason": "string|null",
651 "version": "i64",
652 "request_id": "string|null"
653 },
654 "labels": [
655 "organization_id",
656 "actor_user_id"
657 ],
658 "properties": [
659 "user",
660 "org",
661 "request_id"
662 ],
663 "source": "src/auth/organization/invite.rs:242",
664 "atomicity": "transactional"
665 },
666 {
667 "project": "kenoauth",
668 "topic": "oauth-org",
669 "area": "org",
670 "tag": "redeem-invite",
671 "kind": "organization.invite.create",
672 "payload": {
673 "id": "uuid",
674 "action": "redeem",
675 "user_id": "i64",
676 "joined": "bool",
677 "version": "i64",
678 "request_id": "string|null"
679 },
680 "labels": [
681 "organization_id",
682 "actor_user_id"
683 ],
684 "properties": [
685 "user",
686 "org",
687 "request_id"
688 ],
689 "source": "src/auth/organization/invite.rs:403",
690 "atomicity": "transactional"
691 },
692 {
693 "project": "kenoauth",
694 "topic": "oauth-org",
695 "area": "org",
696 "tag": "add-member",
697 "kind": "organization.member.update",
698 "payload": {
699 "id": "uuid",
700 "user_id": "i64",
701 "action": "add",
702 "created": "bool",
703 "request_id": "string|null"
704 },
705 "labels": [
706 "organization_id",
707 "actor_user_id"
708 ],
709 "properties": [
710 "user",
711 "org",
712 "request_id"
713 ],
714 "source": "src/auth/organization/member.rs:208",
715 "atomicity": "transactional"
716 },
717 {
718 "project": "kenoauth",
719 "topic": "oauth-org",
720 "area": "org",
721 "tag": "remove-member",
722 "kind": "organization.member.update",
723 "payload": {
724 "id": "uuid",
725 "user_id": "i64",
726 "action": "remove",
727 "version": "i64",
728 "request_id": "string|null"
729 },
730 "labels": [
731 "organization_id",
732 "actor_user_id"
733 ],
734 "properties": [
735 "user",
736 "org",
737 "request_id"
738 ],
739 "source": "src/auth/organization/member.rs:280",
740 "atomicity": "transactional"
741 },
742 {
743 "project": "kenoauth",
744 "topic": "oauth-org",
745 "area": "org",
746 "tag": "update-member",
747 "kind": "organization.member.update",
748 "payload": {
749 "id": "uuid",
750 "user_id": "i64",
751 "action": "leave",
752 "version": "i64",
753 "request_id": "string|null"
754 },
755 "labels": [
756 "organization_id",
757 "actor_user_id"
758 ],
759 "properties": [
760 "user",
761 "org",
762 "request_id"
763 ],
764 "source": "src/auth/organization/member.rs:336",
765 "remark": "action=leave 未在 route 中映射,实际 tag 落到默认值 update-member",
766 "atomicity": "transactional"
767 },
768 {
769 "project": "kenoauth",
770 "topic": "oauth-org",
771 "area": "org",
772 "tag": "set-role",
773 "kind": "organization.member.update",
774 "payload": {
775 "id": "uuid",
776 "user_id": "i64",
777 "action": "set_role",
778 "role": "string",
779 "request_id": "string|null"
780 },
781 "labels": [
782 "organization_id",
783 "actor_user_id"
784 ],
785 "properties": [
786 "user",
787 "org",
788 "request_id"
789 ],
790 "source": "src/auth/organization/member.rs:413",
791 "atomicity": "transactional"
792 },
793 {
794 "project": "kenoauth",
795 "topic": "oauth-org",
796 "area": "org",
797 "tag": "ban",
798 "kind": "organization.member.update",
799 "payload": {
800 "id": "uuid",
801 "user_id": "i64",
802 "action": "ban",
803 "reason": "string|null",
804 "request_id": "string|null"
805 },
806 "labels": [
807 "organization_id",
808 "actor_user_id"
809 ],
810 "properties": [
811 "user",
812 "org",
813 "request_id"
814 ],
815 "source": "src/auth/organization/member.rs:484",
816 "atomicity": "transactional"
817 },
818 {
819 "project": "kenoauth",
820 "topic": "oauth-org",
821 "area": "org",
822 "tag": "unban",
823 "kind": "organization.member.update",
824 "payload": {
825 "id": "uuid",
826 "user_id": "i64",
827 "action": "unban",
828 "request_id": "string|null"
829 },
830 "labels": [
831 "organization_id",
832 "actor_user_id"
833 ],
834 "properties": [
835 "user",
836 "org",
837 "request_id"
838 ],
839 "source": "src/auth/organization/member.rs:568",
840 "atomicity": "transactional"
841 },
842 {
843 "project": "kenoauth",
844 "topic": "oauth-org",
845 "area": "org",
846 "tag": "update-member",
847 "kind": "organization.member.update",
848 "payload": {
849 "id": "uuid",
850 "user_id": "i64",
851 "action": "update|self_update",
852 "alias_name": "string|null",
853 "alias_avatar_url": "string|null",
854 "request_id": "string|null"
855 },
856 "labels": [
857 "organization_id",
858 "actor_user_id"
859 ],
860 "properties": [
861 "user",
862 "org",
863 "request_id"
864 ],
865 "source": "src/auth/organization/member.rs:764",
866 "atomicity": "transactional"
867 },
868 {
869 "project": "kenoauth",
870 "topic": "oauth-org",
871 "area": "org",
872 "tag": "update-ban",
873 "kind": "organization.member.update",
874 "payload": {
875 "id": "uuid",
876 "user_id": "i64",
877 "action": "update_ban",
878 "expires_at": "datetime|null",
879 "request_id": "string|null"
880 },
881 "labels": [
882 "organization_id",
883 "actor_user_id"
884 ],
885 "properties": [
886 "user",
887 "org",
888 "request_id"
889 ],
890 "source": "src/auth/organization/member.rs:889",
891 "atomicity": "transactional"
892 },
893 {
894 "project": "kenoauth",
895 "topic": "oauth-org",
896 "area": "org",
897 "tag": "join",
898 "kind": "organization.member.update",
899 "payload": {
900 "id": "uuid",
901 "user_id": "i64",
902 "action": "join_role",
903 "role_id": "uuid",
904 "request_id": "string|null"
905 },
906 "labels": [
907 "organization_id",
908 "actor_user_id"
909 ],
910 "properties": [
911 "user",
912 "org",
913 "request_id"
914 ],
915 "source": "src/auth/organization/role.rs:801",
916 "atomicity": "transactional"
917 },
918 {
919 "project": "kenoauth",
920 "topic": "oauth-org",
921 "area": "org",
922 "tag": "leave",
923 "kind": "organization.member.update",
924 "payload": {
925 "id": "uuid",
926 "user_id": "i64",
927 "action": "leave_role",
928 "role_id": "uuid",
929 "request_id": "string|null"
930 },
931 "labels": [
932 "organization_id",
933 "actor_user_id"
934 ],
935 "properties": [
936 "user",
937 "org",
938 "request_id"
939 ],
940 "source": "src/auth/organization/role.rs:844",
941 "atomicity": "transactional"
942 },
943 {
944 "project": "kenoauth",
945 "topic": "oauth-org",
946 "area": "org",
947 "tag": "create-role",
948 "kind": "organization.role.update",
949 "payload": {
950 "id": "uuid",
951 "key": "string",
952 "action": "create",
953 "request_id": "string|null"
954 },
955 "labels": [
956 "organization_id",
957 "actor_user_id"
958 ],
959 "properties": [
960 "user",
961 "org",
962 "request_id"
963 ],
964 "source": "src/auth/organization/role.rs:306",
965 "atomicity": "transactional"
966 },
967 {
968 "project": "kenoauth",
969 "topic": "oauth-org",
970 "area": "org",
971 "tag": "set-permissions",
972 "kind": "organization.role.update",
973 "payload": {
974 "id": "uuid",
975 "key": "string",
976 "action": "set_permissions",
977 "permissions": "string[]",
978 "request_id": "string|null"
979 },
980 "labels": [
981 "organization_id",
982 "actor_user_id"
983 ],
984 "properties": [
985 "user",
986 "org",
987 "request_id"
988 ],
989 "source": "src/auth/organization/role.rs:367",
990 "atomicity": "transactional"
991 },
992 {
993 "project": "kenoauth",
994 "topic": "oauth-org",
995 "area": "org",
996 "tag": "update-role",
997 "kind": "organization.role.update",
998 "payload": {
999 "id": "uuid",
1000 "key": "string",
1001 "action": "update",
1002 "request_id": "string|null"
1003 },
1004 "labels": [
1005 "organization_id",
1006 "actor_user_id"
1007 ],
1008 "properties": [
1009 "user",
1010 "org",
1011 "request_id"
1012 ],
1013 "source": "src/auth/organization/role.rs:458",
1014 "atomicity": "transactional"
1015 },
1016 {
1017 "project": "kenoauth",
1018 "topic": "oauth-org",
1019 "area": "org",
1020 "tag": "delete-role",
1021 "kind": "organization.role.update",
1022 "payload": {
1023 "id": "uuid",
1024 "key": "string",
1025 "action": "delete",
1026 "version": "i64",
1027 "request_id": "string|null"
1028 },
1029 "labels": [
1030 "organization_id",
1031 "actor_user_id"
1032 ],
1033 "properties": [
1034 "user",
1035 "org",
1036 "request_id"
1037 ],
1038 "source": "src/auth/organization/role.rs:511",
1039 "atomicity": "transactional"
1040 },
1041 {
1042 "project": "kenoauth",
1043 "topic": "oauth-org",
1044 "area": "org",
1045 "tag": "assign-role",
1046 "kind": "organization.role.update",
1047 "payload": {
1048 "member_id": "uuid",
1049 "role_key": "string",
1050 "action": "assign",
1051 "version": "i64",
1052 "request_id": "string|null"
1053 },
1054 "labels": [
1055 "organization_id",
1056 "actor_user_id"
1057 ],
1058 "properties": [
1059 "user",
1060 "org",
1061 "request_id"
1062 ],
1063 "source": "src/auth/organization/role.rs:574",
1064 "atomicity": "transactional"
1065 },
1066 {
1067 "project": "kenoauth",
1068 "topic": "oauth-org",
1069 "area": "org",
1070 "tag": "unassign-role",
1071 "kind": "organization.role.update",
1072 "payload": {
1073 "member_id": "uuid",
1074 "role_key": "string",
1075 "action": "unassign",
1076 "version": "i64",
1077 "request_id": "string|null"
1078 },
1079 "labels": [
1080 "organization_id",
1081 "actor_user_id"
1082 ],
1083 "properties": [
1084 "user",
1085 "org",
1086 "request_id"
1087 ],
1088 "source": "src/auth/organization/role.rs:634",
1089 "atomicity": "transactional"
1090 },
1091 {
1092 "project": "kenoauth",
1093 "topic": "oauth-org",
1094 "area": "org",
1095 "tag": "grant",
1096 "kind": "organization.role.update",
1097 "payload": {
1098 "member_id": "uuid",
1099 "granted": "usize",
1100 "version": "i64",
1101 "request_id": "string|null"
1102 },
1103 "labels": [
1104 "organization_id",
1105 "actor_user_id"
1106 ],
1107 "properties": [
1108 "user",
1109 "org",
1110 "request_id"
1111 ],
1112 "source": "src/auth/organization/role.rs:704",
1113 "remark": "成员直挂权限整表替换;payload 无 action 字段,命中 route 默认值 grant",
1114 "atomicity": "transactional"
1115 },
1116 {
1117 "project": "kenshell",
1118 "topic": "kenshell-repo",
1119 "area": "repo",
1120 "tag": "create",
1121 "kind": "repo.repository.create",
1122 "ordering_key": "repo:{repo_id}",
1123 "labels": [
1124 "repo_id",
1125 "repo_name",
1126 "user_id",
1127 "organization_id",
1128 "request_id"
1129 ],
1130 "properties": [
1131 "repo_id",
1132 "repo_name",
1133 "user",
1134 "org",
1135 "request_id",
1136 "timestamp"
1137 ],
1138 "source": "src/svc/repo/usecase/create.rs:120"
1139 },
1140 {
1141 "project": "kenshell",
1142 "topic": "kenshell-repo",
1143 "area": "repo",
1144 "tag": "update",
1145 "kind": "repo.repository.update",
1146 "ordering_key": "repo:{repo_id}",
1147 "labels": [
1148 "repo_id",
1149 "repo_name",
1150 "user_id",
1151 "organization_id",
1152 "request_id"
1153 ],
1154 "properties": [
1155 "repo_id",
1156 "repo_name",
1157 "user",
1158 "org",
1159 "request_id",
1160 "timestamp"
1161 ],
1162 "source": "src/svc/commands.rs:428"
1163 },
1164 {
1165 "project": "kenshell",
1166 "topic": "kenshell-repo",
1167 "area": "repo",
1168 "tag": "delete",
1169 "kind": "repo.repository.delete",
1170 "ordering_key": "repo:{repo_id}",
1171 "labels": [
1172 "repo_id",
1173 "repo_name",
1174 "user_id",
1175 "organization_id",
1176 "request_id"
1177 ],
1178 "properties": [
1179 "repo_id",
1180 "repo_name",
1181 "user",
1182 "org",
1183 "request_id",
1184 "timestamp"
1185 ],
1186 "source": "src/svc/repo/usecase/delete.rs:89"
1187 },
1188 {
1189 "project": "kenshell",
1190 "topic": "kenshell-repo",
1191 "area": "repo",
1192 "tag": "placement-change",
1193 "kind": "repo.placement.update",
1194 "ordering_key": "repo:{repo_id}",
1195 "labels": [
1196 "repo_id",
1197 "repo_name",
1198 "user_id",
1199 "organization_id",
1200 "request_id"
1201 ],
1202 "properties": [
1203 "repo_id",
1204 "repo_name",
1205 "user",
1206 "org",
1207 "request_id",
1208 "timestamp"
1209 ],
1210 "source": "src/svc/repo/usecase/replicate.rs:167, serve.rs:170, serve.rs:221"
1211 },
1212 {
1213 "project": "kenshell",
1214 "topic": "kenshell-repo",
1215 "area": "repo",
1216 "tag": "language-update",
1217 "kind": "repo.language.update",
1218 "ordering_key": "repo:{repo_id}",
1219 "labels": [
1220 "repo_id",
1221 "repo_name",
1222 "user_id",
1223 "organization_id",
1224 "request_id"
1225 ],
1226 "properties": [
1227 "repo_id",
1228 "repo_name",
1229 "user",
1230 "org",
1231 "request_id",
1232 "timestamp"
1233 ],
1234 "source": "src/svc/git/analysis.rs:155"
1235 },
1236 {
1237 "project": "kenshell",
1238 "topic": "kenshell-ref",
1239 "area": "ref",
1240 "tag": "create-branch",
1241 "kind": "ref.branch.create",
1242 "ordering_key": "repo:{repo_id}",
1243 "labels": [
1244 "repo_id",
1245 "repo_name",
1246 "user_id",
1247 "organization_id",
1248 "request_id"
1249 ],
1250 "properties": [
1251 "repo_id",
1252 "repo_name",
1253 "user",
1254 "org",
1255 "request_id",
1256 "timestamp"
1257 ],
1258 "source": "src/svc/commands.rs:142"
1259 },
1260 {
1261 "project": "kenshell",
1262 "topic": "kenshell-ref",
1263 "area": "ref",
1264 "tag": "update-branch",
1265 "kind": "ref.branch.update",
1266 "ordering_key": "repo:{repo_id}",
1267 "labels": [
1268 "repo_id",
1269 "repo_name",
1270 "user_id",
1271 "organization_id",
1272 "request_id"
1273 ],
1274 "properties": [
1275 "repo_id",
1276 "repo_name",
1277 "user",
1278 "org",
1279 "request_id",
1280 "timestamp"
1281 ],
1282 "source": "src/svc/commands.rs:184"
1283 },
1284 {
1285 "project": "kenshell",
1286 "topic": "kenshell-ref",
1287 "area": "ref",
1288 "tag": "delete-branch",
1289 "kind": "ref.branch.delete",
1290 "ordering_key": "repo:{repo_id}",
1291 "labels": [
1292 "repo_id",
1293 "repo_name",
1294 "user_id",
1295 "organization_id",
1296 "request_id"
1297 ],
1298 "properties": [
1299 "repo_id",
1300 "repo_name",
1301 "user",
1302 "org",
1303 "request_id",
1304 "timestamp"
1305 ],
1306 "source": "src/svc/commands.rs:226"
1307 },
1308 {
1309 "project": "kenshell",
1310 "topic": "kenshell-ref",
1311 "area": "ref",
1312 "tag": "rename-branch",
1313 "kind": "ref.branch.rename",
1314 "ordering_key": "repo:{repo_id}",
1315 "labels": [
1316 "repo_id",
1317 "repo_name",
1318 "user_id",
1319 "organization_id",
1320 "request_id"
1321 ],
1322 "properties": [
1323 "repo_id",
1324 "repo_name",
1325 "user",
1326 "org",
1327 "request_id",
1328 "timestamp"
1329 ],
1330 "source": "src/svc/commands.rs:379"
1331 },
1332 {
1333 "project": "kenshell",
1334 "topic": "kenshell-ref",
1335 "area": "ref",
1336 "tag": "set-default-branch",
1337 "kind": "ref.default_branch.update",
1338 "ordering_key": "repo:{repo_id}",
1339 "labels": [
1340 "repo_id",
1341 "repo_name",
1342 "user_id",
1343 "organization_id",
1344 "request_id"
1345 ],
1346 "properties": [
1347 "repo_id",
1348 "repo_name",
1349 "user",
1350 "org",
1351 "request_id",
1352 "timestamp"
1353 ],
1354 "source": "src/svc/commands.rs:421"
1355 },
1356 {
1357 "project": "kenshell",
1358 "topic": "kenshell-ref",
1359 "area": "ref",
1360 "tag": "create-tag",
1361 "kind": "ref.tag.create",
1362 "ordering_key": "repo:{repo_id}",
1363 "labels": [
1364 "repo_id",
1365 "repo_name",
1366 "user_id",
1367 "organization_id",
1368 "request_id"
1369 ],
1370 "properties": [
1371 "repo_id",
1372 "repo_name",
1373 "user",
1374 "org",
1375 "request_id",
1376 "timestamp"
1377 ],
1378 "source": "src/svc/commands.rs:548"
1379 },
1380 {
1381 "project": "kenshell",
1382 "topic": "kenshell-ref",
1383 "area": "ref",
1384 "tag": "delete-tag",
1385 "kind": "ref.tag.delete",
1386 "ordering_key": "repo:{repo_id}",
1387 "labels": [
1388 "repo_id",
1389 "repo_name",
1390 "user_id",
1391 "organization_id",
1392 "request_id"
1393 ],
1394 "properties": [
1395 "repo_id",
1396 "repo_name",
1397 "user",
1398 "org",
1399 "request_id",
1400 "timestamp"
1401 ],
1402 "source": "src/svc/commands.rs:332"
1403 },
1404 {
1405 "project": "kenshell",
1406 "topic": "kenshell-ref",
1407 "area": "ref",
1408 "tag": "create-protection",
1409 "kind": "ref.protection.create",
1410 "ordering_key": "repo:{repo_id}",
1411 "labels": [
1412 "repo_id",
1413 "repo_name",
1414 "user_id",
1415 "organization_id",
1416 "request_id"
1417 ],
1418 "properties": [
1419 "repo_id",
1420 "repo_name",
1421 "user",
1422 "org",
1423 "request_id",
1424 "timestamp"
1425 ],
1426 "source": "src/svc/git/protection.rs:78"
1427 },
1428 {
1429 "project": "kenshell",
1430 "topic": "kenshell-ref",
1431 "area": "ref",
1432 "tag": "delete-protection",
1433 "kind": "ref.protection.delete",
1434 "ordering_key": "repo:{repo_id}",
1435 "labels": [
1436 "repo_id",
1437 "repo_name",
1438 "user_id",
1439 "organization_id",
1440 "request_id"
1441 ],
1442 "properties": [
1443 "repo_id",
1444 "repo_name",
1445 "user",
1446 "org",
1447 "request_id",
1448 "timestamp"
1449 ],
1450 "source": "src/svc/git/protection.rs:113"
1451 },
1452 {
1453 "project": "kenshell",
1454 "topic": "kenshell-commit",
1455 "area": "commit",
1456 "tag": "sync",
1457 "kind": "commit.commit.sync",
1458 "ordering_key": "repo:{repo_id}",
1459 "labels": [
1460 "repo_id",
1461 "repo_name",
1462 "user_id",
1463 "organization_id",
1464 "request_id"
1465 ],
1466 "properties": [
1467 "repo_id",
1468 "repo_name",
1469 "user",
1470 "org",
1471 "request_id",
1472 "timestamp"
1473 ],
1474 "source": "src/svc/git/metadata.rs:248, src/svc/git/commit.rs:331"
1475 },
1476 {
1477 "project": "kenshell",
1478 "topic": "kenshell-commit",
1479 "area": "commit",
1480 "tag": "set-status",
1481 "kind": "commit.status.set",
1482 "ordering_key": "repo:{repo_id}",
1483 "labels": [
1484 "repo_id",
1485 "repo_name",
1486 "user_id",
1487 "organization_id",
1488 "request_id"
1489 ],
1490 "properties": [
1491 "repo_id",
1492 "repo_name",
1493 "user",
1494 "org",
1495 "request_id",
1496 "timestamp"
1497 ],
1498 "source": "src/svc/git/commit.rs:247"
1499 },
1500 {
1501 "project": "kenshell",
1502 "topic": "kenshell-commit",
1503 "area": "commit",
1504 "tag": "comment-create",
1505 "kind": "commit.code_comment.create",
1506 "ordering_key": "repo:{repo_id}:comment:{comment_id}",
1507 "labels": [
1508 "repo_id",
1509 "repo_name",
1510 "user_id",
1511 "organization_id",
1512 "request_id"
1513 ],
1514 "properties": [
1515 "repo_id",
1516 "repo_name",
1517 "user",
1518 "org",
1519 "request_id",
1520 "timestamp"
1521 ],
1522 "source": "src/svc/comment.rs:87"
1523 },
1524 {
1525 "project": "kenshell",
1526 "topic": "kenshell-commit",
1527 "area": "commit",
1528 "tag": "comment-update",
1529 "kind": "commit.code_comment.update",
1530 "ordering_key": "repo:{repo_id}:comment:{comment_id}",
1531 "labels": [
1532 "repo_id",
1533 "repo_name",
1534 "user_id",
1535 "organization_id",
1536 "request_id"
1537 ],
1538 "properties": [
1539 "repo_id",
1540 "repo_name",
1541 "user",
1542 "org",
1543 "request_id",
1544 "timestamp"
1545 ],
1546 "source": "src/svc/comment.rs:123"
1547 },
1548 {
1549 "project": "kenshell",
1550 "topic": "kenshell-commit",
1551 "area": "commit",
1552 "tag": "comment-delete",
1553 "kind": "commit.code_comment.delete",
1554 "ordering_key": "repo:{repo_id}:comment:{comment_id}",
1555 "labels": [
1556 "repo_id",
1557 "repo_name",
1558 "user_id",
1559 "organization_id",
1560 "request_id"
1561 ],
1562 "properties": [
1563 "repo_id",
1564 "repo_name",
1565 "user",
1566 "org",
1567 "request_id",
1568 "timestamp"
1569 ],
1570 "source": "src/svc/comment.rs:156"
1571 },
1572 {
1573 "project": "kenshell",
1574 "topic": "kenshell-lfs",
1575 "area": "lfs",
1576 "tag": "upload",
1577 "kind": "lfs.object.upload",
1578 "ordering_key": "repo:{repo_id}",
1579 "labels": [
1580 "repo_id",
1581 "repo_name",
1582 "user_id",
1583 "organization_id",
1584 "request_id"
1585 ],
1586 "properties": [
1587 "repo_id",
1588 "repo_name",
1589 "user",
1590 "org",
1591 "request_id",
1592 "timestamp"
1593 ],
1594 "source": "src/svc/lfs/usecase/upload.rs:67,155"
1595 },
1596 {
1597 "project": "kenshell",
1598 "topic": "kenshell-lfs",
1599 "area": "lfs",
1600 "tag": "unreference",
1601 "kind": "lfs.object.unreference",
1602 "ordering_key": "repo:{repo_id}",
1603 "labels": [
1604 "repo_id",
1605 "repo_name",
1606 "user_id",
1607 "organization_id",
1608 "request_id"
1609 ],
1610 "properties": [
1611 "repo_id",
1612 "repo_name",
1613 "user",
1614 "org",
1615 "request_id",
1616 "timestamp"
1617 ],
1618 "source": "src/svc/lfs/usecase/collect.rs:59"
1619 },
1620 {
1621 "project": "kenshell",
1622 "topic": "kenshell-lfs",
1623 "area": "lfs",
1624 "tag": "purge",
1625 "kind": "lfs.object.purge",
1626 "ordering_key": "repo:{repo_id}",
1627 "labels": [
1628 "repo_id",
1629 "repo_name",
1630 "user_id",
1631 "organization_id",
1632 "request_id"
1633 ],
1634 "properties": [
1635 "repo_id",
1636 "repo_name",
1637 "user",
1638 "org",
1639 "request_id",
1640 "timestamp"
1641 ],
1642 "source": "src/svc/lfs/usecase/collect.rs:75"
1643 },
1644 {
1645 "project": "kenshell",
1646 "topic": "kenshell-archive",
1647 "area": "archive",
1648 "tag": "create",
1649 "kind": "archive.artifact.create",
1650 "ordering_key": "repo:{repo_id}",
1651 "labels": [
1652 "repo_id",
1653 "repo_name",
1654 "user_id",
1655 "organization_id",
1656 "request_id"
1657 ],
1658 "properties": [
1659 "repo_id",
1660 "repo_name",
1661 "user",
1662 "org",
1663 "request_id",
1664 "timestamp"
1665 ],
1666 "source": "src/svc/git/archive.rs:356,418"
1667 },
1668 {
1669 "project": "kenshell",
1670 "topic": "kenshell-job",
1671 "area": "job",
1672 "tag": "enqueue",
1673 "kind": "job.job.enqueue",
1674 "ordering_key": "job:{job_id}",
1675 "labels": [
1676 "repo_id",
1677 "repo_name",
1678 "user_id",
1679 "organization_id",
1680 "request_id"
1681 ],
1682 "properties": [
1683 "repo_id",
1684 "repo_name",
1685 "user",
1686 "org",
1687 "request_id",
1688 "timestamp"
1689 ],
1690 "source": "src/svc/jobs.rs:190"
1691 },
1692 {
1693 "project": "kenshell",
1694 "topic": "kenshell-job",
1695 "area": "job",
1696 "tag": "claim",
1697 "kind": "job.job.claim",
1698 "ordering_key": "job:{job_id}",
1699 "labels": [
1700 "repo_id",
1701 "repo_name",
1702 "user_id",
1703 "organization_id",
1704 "request_id"
1705 ],
1706 "properties": [
1707 "repo_id",
1708 "repo_name",
1709 "user",
1710 "org",
1711 "request_id",
1712 "timestamp"
1713 ],
1714 "source": "src/svc/jobs.rs:348"
1715 },
1716 {
1717 "project": "kenshell",
1718 "topic": "kenshell-job",
1719 "area": "job",
1720 "tag": "progress",
1721 "kind": "job.job.progress",
1722 "ordering_key": "job:{job_id}",
1723 "labels": [
1724 "repo_id",
1725 "repo_name",
1726 "user_id",
1727 "organization_id",
1728 "request_id"
1729 ],
1730 "properties": [
1731 "repo_id",
1732 "repo_name",
1733 "user",
1734 "org",
1735 "request_id",
1736 "timestamp"
1737 ],
1738 "source": "src/svc/jobs.rs:376"
1739 },
1740 {
1741 "project": "kenshell",
1742 "topic": "kenshell-job",
1743 "area": "job",
1744 "tag": "heartbeat",
1745 "kind": "job.job.heartbeat",
1746 "ordering_key": "job:{job_id}",
1747 "labels": [
1748 "repo_id",
1749 "repo_name",
1750 "user_id",
1751 "organization_id",
1752 "request_id"
1753 ],
1754 "properties": [
1755 "repo_id",
1756 "repo_name",
1757 "user",
1758 "org",
1759 "request_id",
1760 "timestamp"
1761 ],
1762 "source": "src/svc/jobs.rs:398"
1763 },
1764 {
1765 "project": "kenshell",
1766 "topic": "kenshell-job",
1767 "area": "job",
1768 "tag": "cancel",
1769 "kind": "job.job.cancel",
1770 "ordering_key": "job:{job_id}",
1771 "labels": [
1772 "repo_id",
1773 "repo_name",
1774 "user_id",
1775 "organization_id",
1776 "request_id"
1777 ],
1778 "properties": [
1779 "repo_id",
1780 "repo_name",
1781 "user",
1782 "org",
1783 "request_id",
1784 "timestamp"
1785 ],
1786 "source": "src/svc/jobs.rs:254"
1787 },
1788 {
1789 "project": "kenshell",
1790 "topic": "kenshell-job",
1791 "area": "job",
1792 "tag": "retry",
1793 "kind": "job.job.retry",
1794 "ordering_key": "job:{job_id}",
1795 "labels": [
1796 "repo_id",
1797 "repo_name",
1798 "user_id",
1799 "organization_id",
1800 "request_id"
1801 ],
1802 "properties": [
1803 "repo_id",
1804 "repo_name",
1805 "user",
1806 "org",
1807 "request_id",
1808 "timestamp"
1809 ],
1810 "source": "src/svc/jobs.rs:289"
1811 },
1812 {
1813 "project": "kenshell",
1814 "topic": "kenshell-job",
1815 "area": "job",
1816 "tag": "finish",
1817 "kind": "job.job.finish",
1818 "ordering_key": "job:{job_id} | job:lease-expired",
1819 "labels": [
1820 "repo_id",
1821 "repo_name",
1822 "user_id",
1823 "organization_id",
1824 "request_id"
1825 ],
1826 "properties": [
1827 "repo_id",
1828 "repo_name",
1829 "user",
1830 "org",
1831 "request_id",
1832 "timestamp"
1833 ],
1834 "source": "src/svc/jobs.rs:323,440",
1835 "remark": "job:lease-expired 不是 UUID,无法解析仓库,实际不会有 repo_id/repo_name"
1836 },
1837 {
1838 "project": "kenshell",
1839 "topic": "kenshell-node",
1840 "area": "node",
1841 "tag": "create",
1842 "kind": "node.node.create",
1843 "ordering_key": "node:{node_id}",
1844 "labels": [
1845 "user_id",
1846 "organization_id",
1847 "request_id"
1848 ],
1849 "properties": [
1850 "user",
1851 "org",
1852 "request_id",
1853 "timestamp"
1854 ],
1855 "source": "src/svc/nodes/usecase/create.rs:57"
1856 },
1857 {
1858 "project": "kenshell",
1859 "topic": "kenshell-node",
1860 "area": "node",
1861 "tag": "update",
1862 "kind": "node.node.update",
1863 "ordering_key": "node:{node_id}",
1864 "labels": [
1865 "user_id",
1866 "organization_id",
1867 "request_id"
1868 ],
1869 "properties": [
1870 "user",
1871 "org",
1872 "request_id",
1873 "timestamp"
1874 ],
1875 "source": "src/svc/nodes/usecase/update.rs:93"
1876 },
1877 {
1878 "project": "kenshell",
1879 "topic": "kenshell-node",
1880 "area": "node",
1881 "tag": "delete",
1882 "kind": "node.node.delete",
1883 "ordering_key": "node:{node_id}",
1884 "labels": [
1885 "user_id",
1886 "organization_id",
1887 "request_id"
1888 ],
1889 "properties": [
1890 "user",
1891 "org",
1892 "request_id",
1893 "timestamp"
1894 ],
1895 "source": "src/svc/nodes/usecase/delete.rs:34"
1896 },
1897 {
1898 "project": "kenshell",
1899 "topic": "kenshell-node",
1900 "area": "node",
1901 "tag": "probe",
1902 "kind": "node.node.probe",
1903 "ordering_key": "node:{node_id}",
1904 "labels": [
1905 "user_id",
1906 "organization_id",
1907 "request_id"
1908 ],
1909 "properties": [
1910 "user",
1911 "org",
1912 "request_id",
1913 "timestamp"
1914 ],
1915 "source": "src/svc/nodes/usecase/health.rs:57, src/discovery/reconcile.rs:261,290"
1916 },
1917 {
1918 "project": "kenshell",
1919 "topic": "kenshell-volume",
1920 "area": "volume",
1921 "tag": "create",
1922 "kind": "volume.volume.create",
1923 "ordering_key": "volume:{volume_id}",
1924 "labels": [
1925 "user_id",
1926 "organization_id",
1927 "request_id"
1928 ],
1929 "properties": [
1930 "user",
1931 "org",
1932 "request_id",
1933 "timestamp"
1934 ],
1935 "source": "src/svc/volumes/usecase/create.rs:63"
1936 },
1937 {
1938 "project": "kenshell",
1939 "topic": "kenshell-volume",
1940 "area": "volume",
1941 "tag": "update",
1942 "kind": "volume.volume.update",
1943 "ordering_key": "volume:{volume_id}",
1944 "labels": [
1945 "user_id",
1946 "organization_id",
1947 "request_id"
1948 ],
1949 "properties": [
1950 "user",
1951 "org",
1952 "request_id",
1953 "timestamp"
1954 ],
1955 "source": "src/svc/volumes/usecase/update.rs:83"
1956 },
1957 {
1958 "project": "kenshell",
1959 "topic": "kenshell-volume",
1960 "area": "volume",
1961 "tag": "delete",
1962 "kind": "volume.volume.delete",
1963 "ordering_key": "volume:{volume_id}",
1964 "labels": [
1965 "user_id",
1966 "organization_id",
1967 "request_id"
1968 ],
1969 "properties": [
1970 "user",
1971 "org",
1972 "request_id",
1973 "timestamp"
1974 ],
1975 "source": "src/svc/volumes/usecase/delete.rs:38"
1976 },
1977 {
1978 "project": "kenshell",
1979 "topic": "kenshell-volume",
1980 "area": "volume",
1981 "tag": "mount",
1982 "kind": "volume.mount.create",
1983 "ordering_key": "volume:{volume_id}",
1984 "labels": [
1985 "user_id",
1986 "organization_id",
1987 "request_id"
1988 ],
1989 "properties": [
1990 "user",
1991 "org",
1992 "request_id",
1993 "timestamp"
1994 ],
1995 "source": "src/svc/volumes/usecase/mount.rs:121, src/discovery/reconcile.rs:379"
1996 },
1997 {
1998 "project": "kenshell",
1999 "topic": "kenshell-volume",
2000 "area": "volume",
2001 "tag": "unmount",
2002 "kind": "volume.mount.delete",
2003 "ordering_key": "volume:{volume_id}",
2004 "labels": [
2005 "user_id",
2006 "organization_id",
2007 "request_id"
2008 ],
2009 "properties": [
2010 "user",
2011 "org",
2012 "request_id",
2013 "timestamp"
2014 ],
2015 "source": "src/svc/volumes/usecase/mount.rs:174"
2016 },
2017 {
2018 "project": "kenshell",
2019 "topic": "kenshell-volume",
2020 "area": "volume",
2021 "tag": "probe",
2022 "kind": "volume.volume.probe",
2023 "ordering_key": "volume:{volume_id}",
2024 "labels": [
2025 "user_id",
2026 "organization_id",
2027 "request_id"
2028 ],
2029 "properties": [
2030 "user",
2031 "org",
2032 "request_id",
2033 "timestamp"
2034 ],
2035 "source": "src/svc/volumes/usecase/probe.rs:93"
2036 },
2037 {
2038 "project": "kenapi",
2039 "topic": "gitdata-chat",
2040 "area": "chat",
2041 "tag": "message-create",
2042 "kind": "chat.message.create",
2043 "payload": {
2044 "kind": "message_published",
2045 "room_id": "Uuid",
2046 "seq": "i64",
2047 "message": "MessageDto"
2048 },
2049 "labels": [
2050 "room_id",
2051 "user_id",
2052 "request_id"
2053 ],
2054 "properties": [
2055 "room_id",
2056 "user",
2057 "request_id"
2058 ],
2059 "source": "lib/chat/src/share/notify.rs:313 (callers: lib/chat/src/services/messages.rs:755, lib/chat/src/services/webhooks.rs:413, lib/chat/src/services/rooms.rs:507)",
2060 "atomicity": "post-commit",
2061 "ordering_key": "chat:room:{room_id}"
2062 },
2063 {
2064 "project": "kenapi",
2065 "topic": "gitdata-chat",
2066 "area": "chat",
2067 "tag": "message-update",
2068 "kind": "chat.message.update",
2069 "payload": {
2070 "kind": "message_edited",
2071 "room_id": "Uuid",
2072 "seq": "i64",
2073 "message": "MessageDto"
2074 },
2075 "labels": [
2076 "room_id",
2077 "user_id",
2078 "request_id"
2079 ],
2080 "properties": [
2081 "room_id",
2082 "user",
2083 "request_id"
2084 ],
2085 "source": "lib/chat/src/services/messages.rs:1126, lib/chat/src/services/unfurl.rs:520",
2086 "atomicity": "post-commit",
2087 "ordering_key": "chat:room:{room_id}"
2088 },
2089 {
2090 "project": "kenapi",
2091 "topic": "gitdata-chat",
2092 "area": "chat",
2093 "tag": "message-delete",
2094 "kind": "chat.message.delete",
2095 "payload": {
2096 "kind": "message_deleted",
2097 "room_id": "Uuid",
2098 "seq": "i64",
2099 "message_id": "Uuid"
2100 },
2101 "labels": [
2102 "room_id",
2103 "request_id"
2104 ],
2105 "properties": [
2106 "room_id",
2107 "request_id"
2108 ],
2109 "source": "lib/chat/src/services/messages.rs:1270",
2110 "atomicity": "post-commit",
2111 "ordering_key": "chat:room:{room_id}"
2112 },
2113 {
2114 "project": "kenapi",
2115 "topic": "gitdata-chat",
2116 "area": "chat",
2117 "tag": "messages-delete",
2118 "kind": "chat.messages.delete",
2119 "payload": {
2120 "kind": "messages_deleted",
2121 "room_id": "Uuid",
2122 "seq": "i64",
2123 "message_ids": "Uuid[]"
2124 },
2125 "labels": [
2126 "room_id",
2127 "request_id"
2128 ],
2129 "properties": [
2130 "room_id",
2131 "request_id"
2132 ],
2133 "source": "lib/chat/src/services/messages.rs:1276",
2134 "atomicity": "post-commit",
2135 "ordering_key": "chat:room:{room_id}"
2136 },
2137 {
2138 "project": "kenapi",
2139 "topic": "gitdata-chat",
2140 "area": "chat",
2141 "tag": "pin-create",
2142 "kind": "chat.pin.create",
2143 "payload": {
2144 "kind": "message_pinned",
2145 "room_id": "Uuid",
2146 "seq": "i64",
2147 "message_id": "Uuid",
2148 "pinned": true
2149 },
2150 "labels": [
2151 "room_id",
2152 "request_id"
2153 ],
2154 "properties": [
2155 "room_id",
2156 "request_id"
2157 ],
2158 "source": "lib/chat/src/services/messages.rs:1352",
2159 "atomicity": "post-commit",
2160 "ordering_key": "chat:room:{room_id}"
2161 },
2162 {
2163 "project": "kenapi",
2164 "topic": "gitdata-chat",
2165 "area": "chat",
2166 "tag": "pin-delete",
2167 "kind": "chat.pin.delete",
2168 "payload": {
2169 "kind": "message_pinned",
2170 "room_id": "Uuid",
2171 "seq": "i64",
2172 "message_id": "Uuid",
2173 "pinned": false
2174 },
2175 "labels": [
2176 "room_id",
2177 "request_id"
2178 ],
2179 "properties": [
2180 "room_id",
2181 "request_id"
2182 ],
2183 "source": "lib/chat/src/services/messages.rs:1352",
2184 "atomicity": "post-commit",
2185 "ordering_key": "chat:room:{room_id}"
2186 },
2187 {
2188 "project": "kenapi",
2189 "topic": "gitdata-chat",
2190 "area": "chat",
2191 "tag": "reaction-update",
2192 "kind": "chat.reaction.update",
2193 "payload": {
2194 "kind": "reaction_updated",
2195 "room_id": "Uuid",
2196 "message_id": "Uuid",
2197 "reactions": "ReactionAggDto[]"
2198 },
2199 "labels": [
2200 "room_id"
2201 ],
2202 "properties": [
2203 "room_id"
2204 ],
2205 "source": "lib/chat/src/services/reactions.rs:154",
2206 "atomicity": "post-commit",
2207 "ordering_key": "chat:room:{room_id}"
2208 },
2209 {
2210 "project": "kenapi",
2211 "topic": "gitdata-chat",
2212 "area": "chat",
2213 "tag": "room-update",
2214 "kind": "chat.room.update",
2215 "payload": {
2216 "kind": "room_updated",
2217 "room_id": "Uuid"
2218 },
2219 "labels": [
2220 "room_id"
2221 ],
2222 "properties": [
2223 "room_id"
2224 ],
2225 "source": "lib/chat/src/services/rooms.rs:440",
2226 "atomicity": "post-commit",
2227 "ordering_key": "chat:room:{room_id}"
2228 },
2229 {
2230 "project": "kenapi",
2231 "topic": "gitdata-chat",
2232 "area": "chat",
2233 "tag": "member-create",
2234 "kind": "chat.member.create",
2235 "payload": {
2236 "kind": "member_joined",
2237 "room_id": "Uuid",
2238 "user_id": "i64"
2239 },
2240 "labels": [
2241 "room_id",
2242 "user_id"
2243 ],
2244 "properties": [
2245 "room_id",
2246 "user"
2247 ],
2248 "source": "lib/chat/src/services/rooms.rs:553,694",
2249 "atomicity": "post-commit",
2250 "ordering_key": "chat:room:{room_id}"
2251 },
2252 {
2253 "project": "kenapi",
2254 "topic": "gitdata-chat",
2255 "area": "chat",
2256 "tag": "member-delete",
2257 "kind": "chat.member.delete",
2258 "payload": {
2259 "kind": "member_left",
2260 "room_id": "Uuid",
2261 "user_id": "i64"
2262 },
2263 "labels": [
2264 "room_id",
2265 "user_id"
2266 ],
2267 "properties": [
2268 "room_id",
2269 "user"
2270 ],
2271 "source": "lib/chat/src/services/rooms.rs:604, lib/chat/src/services/dms.rs:409, lib/chat/src/services/room_admin.rs:151,195",
2272 "atomicity": "post-commit",
2273 "ordering_key": "chat:room:{room_id}"
2274 },
2275 {
2276 "project": "kenapi",
2277 "topic": "gitdata-chat",
2278 "area": "chat",
2279 "tag": "thread-update",
2280 "kind": "chat.thread.update",
2281 "payload": {
2282 "kind": "thread_updated",
2283 "room_id": "Uuid",
2284 "thread_id": "Uuid"
2285 },
2286 "labels": [
2287 "room_id"
2288 ],
2289 "properties": [
2290 "room_id"
2291 ],
2292 "source": "lib/chat/src/services/threads.rs:238,348, lib/chat/src/services/messages.rs:759",
2293 "atomicity": "post-commit",
2294 "ordering_key": "chat:room:{room_id}"
2295 },
2296 {
2297 "project": "kenapi",
2298 "topic": "gitdata-chat",
2299 "area": "chat",
2300 "tag": "voice-presence",
2301 "kind": "chat.voice.presence",
2302 "payload": {
2303 "kind": "voice_presence",
2304 "room_id": "Uuid"
2305 },
2306 "labels": [
2307 "room_id"
2308 ],
2309 "properties": [
2310 "room_id"
2311 ],
2312 "source": "lib/chat/src/handlers/voice.rs:444,453,487,500,537",
2313 "atomicity": "post-commit",
2314 "ordering_key": "chat:room:{room_id}"
2315 },
2316 {
2317 "project": "kenapi",
2318 "topic": "gitdata-chat",
2319 "area": "chat",
2320 "tag": "voice-hand",
2321 "kind": "chat.voice.hand",
2322 "payload": {
2323 "kind": "voice_hands",
2324 "room_id": "Uuid"
2325 },
2326 "labels": [
2327 "room_id"
2328 ],
2329 "properties": [
2330 "room_id"
2331 ],
2332 "source": "lib/chat/src/handlers/voice.rs:504, lib/chat/src/services/voice.rs:871,916,988,1056",
2333 "atomicity": "post-commit",
2334 "ordering_key": "chat:room:{room_id}"
2335 },
2336 {
2337 "project": "kenapi",
2338 "topic": "gitdata-chat",
2339 "area": "chat",
2340 "tag": "voice-layout",
2341 "kind": "chat.voice.layout",
2342 "payload": {
2343 "kind": "voice_layout",
2344 "room_id": "Uuid"
2345 },
2346 "labels": [
2347 "room_id"
2348 ],
2349 "properties": [
2350 "room_id"
2351 ],
2352 "source": "lib/chat/src/services/voice.rs:846",
2353 "atomicity": "post-commit",
2354 "ordering_key": "chat:room:{room_id}"
2355 },
2356 {
2357 "project": "kenapi",
2358 "topic": "gitdata-chat",
2359 "area": "chat",
2360 "tag": "voice-moderate",
2361 "kind": "chat.voice.moderate",
2362 "payload": {
2363 "kind": "voice_moderation",
2364 "room_id": "Uuid",
2365 "user_id": "i64",
2366 "action": "string"
2367 },
2368 "labels": [
2369 "room_id",
2370 "user_id"
2371 ],
2372 "properties": [
2373 "room_id",
2374 "user"
2375 ],
2376 "source": "lib/chat/src/services/voice.rs:996,1059,1178,1211",
2377 "atomicity": "post-commit",
2378 "ordering_key": "chat:room:{room_id}"
2379 },
2380 {
2381 "project": "kenapi",
2382 "topic": "gitdata-chat",
2383 "area": "chat",
2384 "tag": "mention-create",
2385 "kind": "chat.mention.create",
2386 "payload": {
2387 "kind": "mention",
2388 "room_id": "Uuid",
2389 "message_id": "Uuid"
2390 },
2391 "labels": [
2392 "user_id",
2393 "room_id",
2394 "request_id"
2395 ],
2396 "properties": [
2397 "user",
2398 "room_id",
2399 "request_id"
2400 ],
2401 "source": "lib/chat/src/share/notify.rs:269, lib/chat/src/services/messages.rs:806,1144",
2402 "atomicity": "post-commit",
2403 "ordering_key": "chat:user:{user_id}"
2404 },
2405 {
2406 "project": "kenapi",
2407 "topic": "gitdata-chat",
2408 "area": "chat",
2409 "tag": "reminder-create",
2410 "kind": "chat.reminder.create",
2411 "payload": {
2412 "kind": "reminder",
2413 "room_id": "Uuid",
2414 "message_id": "Uuid"
2415 },
2416 "labels": [
2417 "user_id",
2418 "room_id",
2419 "request_id"
2420 ],
2421 "properties": [
2422 "user",
2423 "room_id",
2424 "request_id"
2425 ],
2426 "source": "lib/chat/src/services/reminders.rs:273",
2427 "atomicity": "post-commit",
2428 "ordering_key": "chat:user:{user_id}"
2429 },
2430 {
2431 "project": "kenapi",
2432 "topic": "gitdata-chat",
2433 "area": "chat",
2434 "tag": "membership-create",
2435 "kind": "chat.membership.create",
2436 "payload": {
2437 "kind": "room_added",
2438 "room_id": "Uuid"
2439 },
2440 "labels": [
2441 "user_id",
2442 "room_id",
2443 "request_id"
2444 ],
2445 "properties": [
2446 "user",
2447 "room_id",
2448 "request_id"
2449 ],
2450 "source": "lib/chat/src/services/rooms.rs:702",
2451 "atomicity": "post-commit",
2452 "ordering_key": "chat:user:{user_id}"
2453 },
2454 {
2455 "project": "kenapi",
2456 "topic": "gitdata-chat",
2457 "area": "chat",
2458 "tag": "read_state-update",
2459 "kind": "chat.read_state.update",
2460 "payload": {
2461 "kind": "read_marker",
2462 "room_id": "Uuid",
2463 "last_read_message_id": "Uuid|null",
2464 "last_read_seq": "i64"
2465 },
2466 "labels": [
2467 "user_id",
2468 "room_id",
2469 "request_id"
2470 ],
2471 "properties": [
2472 "user",
2473 "room_id",
2474 "request_id"
2475 ],
2476 "source": "lib/chat/src/services/read_state.rs:93",
2477 "atomicity": "post-commit",
2478 "ordering_key": "chat:user:{user_id}"
2479 },
2480 {
2481 "project": "kenapi",
2482 "topic": "gitdata-chat",
2483 "area": "chat",
2484 "tag": "presence-update",
2485 "kind": "chat.presence.update",
2486 "payload": {
2487 "kind": "presence",
2488 "status": "string"
2489 },
2490 "labels": [
2491 "user_id",
2492 "request_id"
2493 ],
2494 "properties": [
2495 "user",
2496 "request_id"
2497 ],
2498 "source": "lib/chat/src/services/presence.rs:118,150,238",
2499 "atomicity": "post-commit",
2500 "ordering_key": "chat:user:{user_id}"
2501 },
2502 {
2503 "project": "kenapi",
2504 "topic": "gitdata-notify",
2505 "area": "notify",
2506 "tag": "notification-create",
2507 "kind": "notify.notification.create",
2508 "payload": {
2509 "kind": "notification",
2510 "notification": "NotificationDto"
2511 },
2512 "labels": [
2513 "user_id",
2514 "request_id"
2515 ],
2516 "properties": [
2517 "user",
2518 "request_id"
2519 ],
2520 "source": "lib/export/src/notify.rs:49",
2521 "atomicity": "post-commit",
2522 "ordering_key": "notify:user:{user_id}"
2523 },
2524 {
2525 "project": "kenapi",
2526 "topic": "gitdata-issue",
2527 "area": "issue",
2528 "tag": "issue-create",
2529 "kind": "issue.issue.create",
2530 "payload": {
2531 "issue_id": "Uuid",
2532 "user_id": "i64",
2533 "event_type": "created",
2534 "comment_id": "Uuid|null",
2535 "old_title": "string|null",
2536 "new_title": "string|null",
2537 "content": "string|null"
2538 },
2539 "labels": [
2540 "issue_id",
2541 "user_id",
2542 "request_id"
2543 ],
2544 "properties": [
2545 "issue_id",
2546 "user",
2547 "request_id"
2548 ],
2549 "source": "lib/issues/src/services/issues.rs:608",
2550 "atomicity": "same connection as issue_event_line",
2551 "ordering_key": "issue:{issue_id}"
2552 },
2553 {
2554 "project": "kenapi",
2555 "topic": "gitdata-issue",
2556 "area": "issue",
2557 "tag": "title-update",
2558 "kind": "issue.title.update",
2559 "payload": {
2560 "issue_id": "Uuid",
2561 "user_id": "i64",
2562 "event_type": "title_updated",
2563 "comment_id": "Uuid|null",
2564 "old_title": "string|null",
2565 "new_title": "string|null",
2566 "content": "string|null"
2567 },
2568 "labels": [
2569 "issue_id",
2570 "user_id",
2571 "request_id"
2572 ],
2573 "properties": [
2574 "issue_id",
2575 "user",
2576 "request_id"
2577 ],
2578 "source": "lib/issues/src/services/issues.rs:1040",
2579 "atomicity": "same connection as issue_event_line",
2580 "ordering_key": "issue:{issue_id}"
2581 },
2582 {
2583 "project": "kenapi",
2584 "topic": "gitdata-issue",
2585 "area": "issue",
2586 "tag": "milestone-update",
2587 "kind": "issue.milestone.update",
2588 "payload": {
2589 "issue_id": "Uuid",
2590 "user_id": "i64",
2591 "event_type": "milestone_updated",
2592 "comment_id": "Uuid|null",
2593 "old_title": "string|null",
2594 "new_title": "string|null",
2595 "content": "string|null"
2596 },
2597 "labels": [
2598 "issue_id",
2599 "user_id",
2600 "request_id"
2601 ],
2602 "properties": [
2603 "issue_id",
2604 "user",
2605 "request_id"
2606 ],
2607 "source": "lib/issues/src/services/issues.rs:1112",
2608 "atomicity": "same connection as issue_event_line",
2609 "ordering_key": "issue:{issue_id}"
2610 },
2611 {
2612 "project": "kenapi",
2613 "topic": "gitdata-issue",
2614 "area": "issue",
2615 "tag": "issue-close",
2616 "kind": "issue.issue.close",
2617 "payload": {
2618 "issue_id": "Uuid",
2619 "user_id": "i64",
2620 "event_type": "closed",
2621 "comment_id": "Uuid|null",
2622 "old_title": "string|null",
2623 "new_title": "string|null",
2624 "content": "string|null"
2625 },
2626 "labels": [
2627 "issue_id",
2628 "user_id",
2629 "request_id"
2630 ],
2631 "properties": [
2632 "issue_id",
2633 "user",
2634 "request_id"
2635 ],
2636 "source": "lib/issues/src/services/issues.rs:1197,1291",
2637 "atomicity": "same connection as issue_event_line",
2638 "ordering_key": "issue:{issue_id}"
2639 },
2640 {
2641 "project": "kenapi",
2642 "topic": "gitdata-issue",
2643 "area": "issue",
2644 "tag": "issue-reopen",
2645 "kind": "issue.issue.reopen",
2646 "payload": {
2647 "issue_id": "Uuid",
2648 "user_id": "i64",
2649 "event_type": "reopened",
2650 "comment_id": "Uuid|null",
2651 "old_title": "string|null",
2652 "new_title": "string|null",
2653 "content": "string|null"
2654 },
2655 "labels": [
2656 "issue_id",
2657 "user_id",
2658 "request_id"
2659 ],
2660 "properties": [
2661 "issue_id",
2662 "user",
2663 "request_id"
2664 ],
2665 "source": "lib/issues/src/services/issues.rs:1197,1291",
2666 "atomicity": "same connection as issue_event_line",
2667 "ordering_key": "issue:{issue_id}"
2668 },
2669 {
2670 "project": "kenapi",
2671 "topic": "gitdata-issue",
2672 "area": "issue",
2673 "tag": "comment-create",
2674 "kind": "issue.comment.create",
2675 "payload": {
2676 "issue_id": "Uuid",
2677 "user_id": "i64",
2678 "event_type": "comment_created",
2679 "comment_id": "Uuid|null",
2680 "old_title": "string|null",
2681 "new_title": "string|null",
2682 "content": "string|null"
2683 },
2684 "labels": [
2685 "issue_id",
2686 "user_id",
2687 "request_id"
2688 ],
2689 "properties": [
2690 "issue_id",
2691 "user",
2692 "request_id"
2693 ],
2694 "source": "lib/issues/src/services/comments.rs:157",
2695 "atomicity": "same connection as issue_event_line",
2696 "ordering_key": "issue:{issue_id}"
2697 },
2698 {
2699 "project": "kenapi",
2700 "topic": "gitdata-issue",
2701 "area": "issue",
2702 "tag": "comment-update",
2703 "kind": "issue.comment.update",
2704 "payload": {
2705 "issue_id": "Uuid",
2706 "user_id": "i64",
2707 "event_type": "comment_updated",
2708 "comment_id": "Uuid|null",
2709 "old_title": "string|null",
2710 "new_title": "string|null",
2711 "content": "string|null"
2712 },
2713 "labels": [
2714 "issue_id",
2715 "user_id",
2716 "request_id"
2717 ],
2718 "properties": [
2719 "issue_id",
2720 "user",
2721 "request_id"
2722 ],
2723 "source": "lib/issues/src/services/comments.rs:500",
2724 "atomicity": "same connection as issue_event_line",
2725 "ordering_key": "issue:{issue_id}"
2726 },
2727 {
2728 "project": "kenapi",
2729 "topic": "gitdata-issue",
2730 "area": "issue",
2731 "tag": "comment-delete",
2732 "kind": "issue.comment.delete",
2733 "payload": {
2734 "issue_id": "Uuid",
2735 "user_id": "i64",
2736 "event_type": "comment_deleted",
2737 "comment_id": "Uuid|null",
2738 "old_title": "string|null",
2739 "new_title": "string|null",
2740 "content": "string|null"
2741 },
2742 "labels": [
2743 "issue_id",
2744 "user_id",
2745 "request_id"
2746 ],
2747 "properties": [
2748 "issue_id",
2749 "user",
2750 "request_id"
2751 ],
2752 "source": "lib/issues/src/services/comments.rs:567",
2753 "atomicity": "same connection as issue_event_line",
2754 "ordering_key": "issue:{issue_id}"
2755 },
2756 {
2757 "project": "kenapi",
2758 "topic": "gitdata-issue",
2759 "area": "issue",
2760 "tag": "member-create",
2761 "kind": "issue.member.create",
2762 "payload": {
2763 "issue_id": "Uuid",
2764 "user_id": "i64",
2765 "event_type": "member_added",
2766 "comment_id": "Uuid|null",
2767 "old_title": "string|null",
2768 "new_title": "string|null",
2769 "content": "string|null"
2770 },
2771 "labels": [
2772 "issue_id",
2773 "user_id",
2774 "request_id"
2775 ],
2776 "properties": [
2777 "issue_id",
2778 "user",
2779 "request_id"
2780 ],
2781 "source": "lib/issues/src/services/members.rs:122",
2782 "atomicity": "same connection as issue_event_line",
2783 "ordering_key": "issue:{issue_id}"
2784 },
2785 {
2786 "project": "kenapi",
2787 "topic": "gitdata-issue",
2788 "area": "issue",
2789 "tag": "member-delete",
2790 "kind": "issue.member.delete",
2791 "payload": {
2792 "issue_id": "Uuid",
2793 "user_id": "i64",
2794 "event_type": "member_removed",
2795 "comment_id": "Uuid|null",
2796 "old_title": "string|null",
2797 "new_title": "string|null",
2798 "content": "string|null"
2799 },
2800 "labels": [
2801 "issue_id",
2802 "user_id",
2803 "request_id"
2804 ],
2805 "properties": [
2806 "issue_id",
2807 "user",
2808 "request_id"
2809 ],
2810 "source": "lib/issues/src/services/members.rs:287",
2811 "atomicity": "same connection as issue_event_line",
2812 "ordering_key": "issue:{issue_id}"
2813 },
2814 {
2815 "project": "kenapi",
2816 "topic": "gitdata-issue",
2817 "area": "issue",
2818 "tag": "label-create",
2819 "kind": "issue.label.create",
2820 "payload": {
2821 "issue_id": "Uuid",
2822 "user_id": "i64",
2823 "event_type": "label_added",
2824 "comment_id": "Uuid|null",
2825 "old_title": "string|null",
2826 "new_title": "string|null",
2827 "content": "string|null"
2828 },
2829 "labels": [
2830 "issue_id",
2831 "user_id",
2832 "request_id"
2833 ],
2834 "properties": [
2835 "issue_id",
2836 "user",
2837 "request_id"
2838 ],
2839 "source": "lib/issues/src/services/labels.rs:404",
2840 "atomicity": "same connection as issue_event_line",
2841 "ordering_key": "issue:{issue_id}"
2842 },
2843 {
2844 "project": "kenapi",
2845 "topic": "gitdata-issue",
2846 "area": "issue",
2847 "tag": "label-delete",
2848 "kind": "issue.label.delete",
2849 "payload": {
2850 "issue_id": "Uuid",
2851 "user_id": "i64",
2852 "event_type": "label_removed",
2853 "comment_id": "Uuid|null",
2854 "old_title": "string|null",
2855 "new_title": "string|null",
2856 "content": "string|null"
2857 },
2858 "labels": [
2859 "issue_id",
2860 "user_id",
2861 "request_id"
2862 ],
2863 "properties": [
2864 "issue_id",
2865 "user",
2866 "request_id"
2867 ],
2868 "source": "lib/issues/src/services/labels.rs:458",
2869 "atomicity": "same connection as issue_event_line",
2870 "ordering_key": "issue:{issue_id}"
2871 },
2872 {
2873 "project": "kenapi",
2874 "topic": "gitdata-issue",
2875 "area": "issue",
2876 "tag": "dependency-create",
2877 "kind": "issue.dependency.create",
2878 "payload": {
2879 "issue_id": "Uuid",
2880 "user_id": "i64",
2881 "event_type": "dependency_added",
2882 "comment_id": "Uuid|null",
2883 "old_title": "string|null",
2884 "new_title": "string|null",
2885 "content": "string|null"
2886 },
2887 "labels": [
2888 "issue_id",
2889 "user_id",
2890 "request_id"
2891 ],
2892 "properties": [
2893 "issue_id",
2894 "user",
2895 "request_id"
2896 ],
2897 "source": "lib/issues/src/services/dependencies.rs:102",
2898 "atomicity": "same connection as issue_event_line",
2899 "ordering_key": "issue:{issue_id}"
2900 },
2901 {
2902 "project": "kenapi",
2903 "topic": "gitdata-issue",
2904 "area": "issue",
2905 "tag": "dependency-delete",
2906 "kind": "issue.dependency.delete",
2907 "payload": {
2908 "issue_id": "Uuid",
2909 "user_id": "i64",
2910 "event_type": "dependency_removed",
2911 "comment_id": "Uuid|null",
2912 "old_title": "string|null",
2913 "new_title": "string|null",
2914 "content": "string|null"
2915 },
2916 "labels": [
2917 "issue_id",
2918 "user_id",
2919 "request_id"
2920 ],
2921 "properties": [
2922 "issue_id",
2923 "user",
2924 "request_id"
2925 ],
2926 "source": "lib/issues/src/services/dependencies.rs:191",
2927 "atomicity": "same connection as issue_event_line",
2928 "ordering_key": "issue:{issue_id}"
2929 },
2930 {
2931 "project": "kenapi",
2932 "topic": "gitdata-pull",
2933 "area": "pull",
2934 "tag": "pull-create",
2935 "kind": "pull.pull.create",
2936 "payload": {
2937 "pull_request_id": "Uuid",
2938 "user_id": "i64",
2939 "event_type": "created",
2940 "commit_sha": "string|null",
2941 "content": "string|null"
2942 },
2943 "labels": [
2944 "pull_id",
2945 "user_id",
2946 "request_id"
2947 ],
2948 "properties": [
2949 "pull_id",
2950 "user",
2951 "request_id"
2952 ],
2953 "source": "lib/pulls/src/services/pulls.rs:404",
2954 "atomicity": "same connection as pull_request_event_line",
2955 "ordering_key": "pull:{pull_request_id}"
2956 },
2957 {
2958 "project": "kenapi",
2959 "topic": "gitdata-pull",
2960 "area": "pull",
2961 "tag": "pull-update",
2962 "kind": "pull.pull.update",
2963 "payload": {
2964 "pull_request_id": "Uuid",
2965 "user_id": "i64",
2966 "event_type": "updated",
2967 "commit_sha": "string|null",
2968 "content": "string|null"
2969 },
2970 "labels": [
2971 "pull_id",
2972 "user_id",
2973 "request_id"
2974 ],
2975 "properties": [
2976 "pull_id",
2977 "user",
2978 "request_id"
2979 ],
2980 "source": "lib/pulls/src/services/pulls.rs:501",
2981 "atomicity": "same connection as pull_request_event_line",
2982 "ordering_key": "pull:{pull_request_id}"
2983 },
2984 {
2985 "project": "kenapi",
2986 "topic": "gitdata-pull",
2987 "area": "pull",
2988 "tag": "pull-close",
2989 "kind": "pull.pull.close",
2990 "payload": {
2991 "pull_request_id": "Uuid",
2992 "user_id": "i64",
2993 "event_type": "closed",
2994 "commit_sha": "string|null",
2995 "content": "string|null"
2996 },
2997 "labels": [
2998 "pull_id",
2999 "user_id",
3000 "request_id"
3001 ],
3002 "properties": [
3003 "pull_id",
3004 "user",
3005 "request_id"
3006 ],
3007 "source": "lib/pulls/src/services/pulls.rs:532",
3008 "atomicity": "same connection as pull_request_event_line",
3009 "ordering_key": "pull:{pull_request_id}"
3010 },
3011 {
3012 "project": "kenapi",
3013 "topic": "gitdata-pull",
3014 "area": "pull",
3015 "tag": "pull-reopen",
3016 "kind": "pull.pull.reopen",
3017 "payload": {
3018 "pull_request_id": "Uuid",
3019 "user_id": "i64",
3020 "event_type": "reopened",
3021 "commit_sha": "string|null",
3022 "content": "string|null"
3023 },
3024 "labels": [
3025 "pull_id",
3026 "user_id",
3027 "request_id"
3028 ],
3029 "properties": [
3030 "pull_id",
3031 "user",
3032 "request_id"
3033 ],
3034 "source": "lib/pulls/src/services/pulls.rs:532",
3035 "atomicity": "same connection as pull_request_event_line",
3036 "ordering_key": "pull:{pull_request_id}"
3037 },
3038 {
3039 "project": "kenapi",
3040 "topic": "gitdata-pull",
3041 "area": "pull",
3042 "tag": "pull-merge",
3043 "kind": "pull.pull.merge",
3044 "payload": {
3045 "pull_request_id": "Uuid",
3046 "user_id": "i64",
3047 "event_type": "merged",
3048 "commit_sha": "string|null",
3049 "content": "string|null"
3050 },
3051 "labels": [
3052 "pull_id",
3053 "user_id",
3054 "request_id"
3055 ],
3056 "properties": [
3057 "pull_id",
3058 "user",
3059 "request_id"
3060 ],
3061 "source": "lib/pulls/src/services/pulls.rs:673",
3062 "atomicity": "same connection as pull_request_event_line",
3063 "ordering_key": "pull:{pull_request_id}"
3064 },
3065 {
3066 "project": "kenapi",
3067 "topic": "gitdata-pull",
3068 "area": "pull",
3069 "tag": "comment-create",
3070 "kind": "pull.comment.create",
3071 "payload": {
3072 "pull_request_id": "Uuid",
3073 "user_id": "i64",
3074 "event_type": "commented",
3075 "commit_sha": "string|null",
3076 "content": "string|null"
3077 },
3078 "labels": [
3079 "pull_id",
3080 "user_id",
3081 "request_id"
3082 ],
3083 "properties": [
3084 "pull_id",
3085 "user",
3086 "request_id"
3087 ],
3088 "source": "lib/pulls/src/services/comments.rs:169",
3089 "atomicity": "same connection as pull_request_event_line",
3090 "ordering_key": "pull:{pull_request_id}"
3091 },
3092 {
3093 "project": "kenapi",
3094 "topic": "gitdata-pull",
3095 "area": "pull",
3096 "tag": "review-create",
3097 "kind": "pull.review.create",
3098 "payload": {
3099 "pull_request_id": "Uuid",
3100 "user_id": "i64",
3101 "event_type": "review_submitted",
3102 "commit_sha": "string|null",
3103 "content": "string|null"
3104 },
3105 "labels": [
3106 "pull_id",
3107 "user_id",
3108 "request_id"
3109 ],
3110 "properties": [
3111 "pull_id",
3112 "user",
3113 "request_id"
3114 ],
3115 "source": "lib/pulls/src/services/reviews.rs:139",
3116 "atomicity": "same connection as pull_request_event_line",
3117 "ordering_key": "pull:{pull_request_id}"
3118 },
3119 {
3120 "project": "kenapi",
3121 "topic": "gitdata-pull",
3122 "area": "pull",
3123 "tag": "review-delete",
3124 "kind": "pull.review.delete",
3125 "payload": {
3126 "pull_request_id": "Uuid",
3127 "user_id": "i64",
3128 "event_type": "review_dismissed",
3129 "commit_sha": "string|null",
3130 "content": "string|null"
3131 },
3132 "labels": [
3133 "pull_id",
3134 "user_id",
3135 "request_id"
3136 ],
3137 "properties": [
3138 "pull_id",
3139 "user",
3140 "request_id"
3141 ],
3142 "source": "lib/pulls/src/services/reviews.rs:232",
3143 "atomicity": "same connection as pull_request_event_line",
3144 "ordering_key": "pull:{pull_request_id}"
3145 },
3146 {
3147 "project": "kenapi",
3148 "topic": "gitdata-pull",
3149 "area": "pull",
3150 "tag": "review-request-create",
3151 "kind": "pull.review-request.create",
3152 "payload": {
3153 "pull_request_id": "Uuid",
3154 "user_id": "i64",
3155 "event_type": "review_requested",
3156 "commit_sha": "string|null",
3157 "content": "string|null"
3158 },
3159 "labels": [
3160 "pull_id",
3161 "user_id",
3162 "request_id"
3163 ],
3164 "properties": [
3165 "pull_id",
3166 "user",
3167 "request_id"
3168 ],
3169 "source": "lib/pulls/src/services/reviewers.rs:131",
3170 "atomicity": "same connection as pull_request_event_line",
3171 "ordering_key": "pull:{pull_request_id}"
3172 },
3173 {
3174 "project": "kenapi",
3175 "topic": "gitdata-pull",
3176 "area": "pull",
3177 "tag": "review-request-delete",
3178 "kind": "pull.review-request.delete",
3179 "payload": {
3180 "pull_request_id": "Uuid",
3181 "user_id": "i64",
3182 "event_type": "review_request_cancelled",
3183 "commit_sha": "string|null",
3184 "content": "string|null"
3185 },
3186 "labels": [
3187 "pull_id",
3188 "user_id",
3189 "request_id"
3190 ],
3191 "properties": [
3192 "pull_id",
3193 "user",
3194 "request_id"
3195 ],
3196 "source": "lib/pulls/src/services/reviewers.rs:165",
3197 "atomicity": "same connection as pull_request_event_line",
3198 "ordering_key": "pull:{pull_request_id}"
3199 },
3200 {
3201 "project": "kenapi",
3202 "topic": "gitdata-pull",
3203 "area": "pull",
3204 "tag": "head-update",
3205 "kind": "pull.head.update",
3206 "payload": {
3207 "pull_request_id": "Uuid",
3208 "user_id": "i64",
3209 "event_type": "head_updated",
3210 "commit_sha": "string|null",
3211 "content": "string|null"
3212 },
3213 "labels": [
3214 "pull_id",
3215 "user_id",
3216 "request_id"
3217 ],
3218 "properties": [
3219 "pull_id",
3220 "user",
3221 "request_id"
3222 ],
3223 "source": "lib/pulls/src/services/changes.rs:147",
3224 "atomicity": "same connection as pull_request_event_line",
3225 "ordering_key": "pull:{pull_request_id}"
3226 },
3227 {
3228 "project": "kenapi",
3229 "topic": "gitdata-pull",
3230 "area": "pull",
3231 "tag": "label-create",
3232 "kind": "pull.label.create",
3233 "payload": {
3234 "pull_request_id": "Uuid",
3235 "user_id": "i64",
3236 "event_type": "labeled",
3237 "commit_sha": "string|null",
3238 "content": "string|null"
3239 },
3240 "labels": [
3241 "pull_id",
3242 "user_id",
3243 "request_id"
3244 ],
3245 "properties": [
3246 "pull_id",
3247 "user",
3248 "request_id"
3249 ],
3250 "source": "lib/pulls/src/services/labels.rs:106",
3251 "atomicity": "same connection as pull_request_event_line",
3252 "ordering_key": "pull:{pull_request_id}"
3253 },
3254 {
3255 "project": "kenapi",
3256 "topic": "gitdata-pull",
3257 "area": "pull",
3258 "tag": "label-delete",
3259 "kind": "pull.label.delete",
3260 "payload": {
3261 "pull_request_id": "Uuid",
3262 "user_id": "i64",
3263 "event_type": "unlabeled",
3264 "commit_sha": "string|null",
3265 "content": "string|null"
3266 },
3267 "labels": [
3268 "pull_id",
3269 "user_id",
3270 "request_id"
3271 ],
3272 "properties": [
3273 "pull_id",
3274 "user",
3275 "request_id"
3276 ],
3277 "source": "lib/pulls/src/services/labels.rs:133",
3278 "atomicity": "same connection as pull_request_event_line",
3279 "ordering_key": "pull:{pull_request_id}"
3280 },
3281 {
3282 "project": "kenapi",
3283 "topic": "gitdata-pull",
3284 "area": "pull",
3285 "tag": "assignee-create",
3286 "kind": "pull.assignee.create",
3287 "payload": {
3288 "pull_request_id": "Uuid",
3289 "user_id": "i64",
3290 "event_type": "assigned",
3291 "commit_sha": "string|null",
3292 "content": "string|null"
3293 },
3294 "labels": [
3295 "pull_id",
3296 "user_id",
3297 "request_id"
3298 ],
3299 "properties": [
3300 "pull_id",
3301 "user",
3302 "request_id"
3303 ],
3304 "source": "lib/pulls/src/services/assignees.rs:72",
3305 "atomicity": "same connection as pull_request_event_line",
3306 "ordering_key": "pull:{pull_request_id}"
3307 },
3308 {
3309 "project": "kenapi",
3310 "topic": "gitdata-pull",
3311 "area": "pull",
3312 "tag": "assignee-delete",
3313 "kind": "pull.assignee.delete",
3314 "payload": {
3315 "pull_request_id": "Uuid",
3316 "user_id": "i64",
3317 "event_type": "unassigned",
3318 "commit_sha": "string|null",
3319 "content": "string|null"
3320 },
3321 "labels": [
3322 "pull_id",
3323 "user_id",
3324 "request_id"
3325 ],
3326 "properties": [
3327 "pull_id",
3328 "user",
3329 "request_id"
3330 ],
3331 "source": "lib/pulls/src/services/assignees.rs:98",
3332 "atomicity": "same connection as pull_request_event_line",
3333 "ordering_key": "pull:{pull_request_id}"
3334 },
3335 {
3336 "project": "kenapi",
3337 "topic": "gitdata-pull",
3338 "area": "pull",
3339 "tag": "lock-create",
3340 "kind": "pull.lock.create",
3341 "payload": {
3342 "pull_request_id": "Uuid",
3343 "user_id": "i64",
3344 "event_type": "locked",
3345 "commit_sha": "string|null",
3346 "content": "string|null"
3347 },
3348 "labels": [
3349 "pull_id",
3350 "user_id",
3351 "request_id"
3352 ],
3353 "properties": [
3354 "pull_id",
3355 "user",
3356 "request_id"
3357 ],
3358 "source": "lib/pulls/src/services/pulls.rs:694",
3359 "atomicity": "same connection as pull_request_event_line",
3360 "ordering_key": "pull:{pull_request_id}"
3361 },
3362 {
3363 "project": "kenapi",
3364 "topic": "gitdata-pull",
3365 "area": "pull",
3366 "tag": "lock-delete",
3367 "kind": "pull.lock.delete",
3368 "payload": {
3369 "pull_request_id": "Uuid",
3370 "user_id": "i64",
3371 "event_type": "unlocked",
3372 "commit_sha": "string|null",
3373 "content": "string|null"
3374 },
3375 "labels": [
3376 "pull_id",
3377 "user_id",
3378 "request_id"
3379 ],
3380 "properties": [
3381 "pull_id",
3382 "user",
3383 "request_id"
3384 ],
3385 "source": "lib/pulls/src/services/pulls.rs:694",
3386 "atomicity": "same connection as pull_request_event_line",
3387 "ordering_key": "pull:{pull_request_id}"
3388 }
3389 ]
3390}
Added lib/events/src/gateway/action/common.rs View lib/events/src/gateway/action/common.rs
@@ -0,0 +1,124 @@
1use futures_util::TryStreamExt;
2use rocketmq_common::common::message::MessageTrait;
3use rocketmq_common::common::message::message_ext::MessageExt;
4use serde_json::Value;
5
6use super::{EventProperties, gitdata, kenshell, platform_events};
7use crate::gateway::platform::PlatformClient;
8use crate::gateway::{ConsumeResult, Subscription, Subscriptions};
9use actor::ActorHost;
10use std::collections::HashMap;
11
12pub struct EventContext<'a> {
13 pub host: &'a ActorHost,
14 pub platform: &'a PlatformClient,
15 pub subscriptions: &'a Subscriptions,
16}
17
18pub async fn action(message: MessageExt, context: &EventContext<'_>) -> ConsumeResult {
19 let topic = message.topic().to_string();
20 let tag = message
21 .tags_ref()
22 .map(ToString::to_string)
23 .unwrap_or_default();
24 let properties = EventProperties::from_message(&message);
25
26 match topic.split_once('-').map(|(prefix, _)| prefix) {
27 Some("gitdata") if topic == "gitdata-auth" || topic == "gitdata-org" => {
28 platform_events::route(&topic, &tag, message, properties, context).await
29 }
30 Some("kenshell") => kenshell::route(&topic, &tag, message, properties, context).await,
31 Some("gitdata") => gitdata::route(&topic, &tag, message, properties, context).await,
32 _ => unimplemented(message),
33 }
34}
35
36pub(super) async fn deliver_to_subscribers(
37 topic: &str,
38 tag: &str,
39 message: MessageExt,
40 properties: EventProperties,
41 context: &EventContext<'_>,
42) -> ConsumeResult {
43 let event_user_id = user_id(&properties);
44 let body = body(&message);
45 let subscribers = match context
46 .subscriptions
47 .subscribers(topic, tag, &properties)
48 .await
49 {
50 Ok(subscribers) => subscribers,
51 Err(_) => return retry(message),
52 };
53 let mut recipients: HashMap<i64, Vec<Subscription>> = HashMap::new();
54 for subscription in subscribers {
55 if subscription.filter.matches(&properties) {
56 recipients
57 .entry(subscription.user_id)
58 .or_default()
59 .push(subscription);
60 }
61 }
62 let properties = &properties;
63 let body = &body;
64 let delivered = futures_util::stream::iter(recipients.into_iter().map(Ok::<_, ()>))
65 .try_for_each_concurrent(32, |(recipient, destinations)| async move {
66 let mut value = payload(topic, tag, event_user_id, recipient, properties, body);
67 value["destinations"] = serde_json::to_value(destinations).map_err(|_| ())?;
68 context
69 .host
70 .publish_event(recipient, value)
71 .await
72 .map_err(|_| ())
73 })
74 .await;
75 if delivered.is_err() {
76 return retry(message);
77 }
78 ConsumeResult::Ack { message }
79}
80
81pub(super) fn payload(
82 topic: &str,
83 tag: &str,
84 event_user_id: Option<i64>,
85 recipient_user_id: i64,
86 properties: &EventProperties,
87 body: &Value,
88) -> Value {
89 serde_json::json!({
90 "topic": topic,
91 "tag": tag,
92 "user_id": event_user_id,
93 "recipient_user_id": recipient_user_id,
94 "properties": properties,
95 "body": body,
96 })
97}
98
99pub(super) fn user_id(properties: &EventProperties) -> Option<i64> {
100 properties
101 .user_id
102 .as_deref()?
103 .parse::<i64>()
104 .ok()
105 .filter(|id| *id > 0)
106}
107
108pub(super) fn body(message: &MessageExt) -> Value {
109 message
110 .body()
111 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
112 .unwrap_or(Value::Null)
113}
114
115pub(super) fn retry(message: MessageExt) -> ConsumeResult {
116 ConsumeResult::Retry {
117 message,
118 delay_level: 0,
119 }
120}
121
122pub(super) fn unimplemented(message: MessageExt) -> ConsumeResult {
123 retry(message)
124}
Added lib/events/src/gateway/action/gitdata.rs View lib/events/src/gateway/action/gitdata.rs
@@ -0,0 +1,77 @@
1use rocketmq_common::common::message::message_ext::MessageExt;
2
3use super::EventProperties;
4use super::common::{EventContext, deliver_to_subscribers, unimplemented};
5use crate::gateway::ConsumeResult;
6
7pub(super) async fn route(
8 topic: &str,
9 tag: &str,
10 message: MessageExt,
11 properties: EventProperties,
12 context: &EventContext<'_>,
13) -> ConsumeResult {
14 if !is_supported(topic, tag) {
15 return unimplemented(message);
16 }
17
18 deliver_to_subscribers(topic, tag, message, properties, context).await
19}
20
21pub(super) fn is_supported(topic: &str, tag: &str) -> bool {
22 matches!(
23 (topic, tag),
24 ("gitdata-chat", "message-create")
25 | ("gitdata-chat", "message-update")
26 | ("gitdata-chat", "message-delete")
27 | ("gitdata-chat", "messages-delete")
28 | ("gitdata-chat", "pin-create")
29 | ("gitdata-chat", "pin-delete")
30 | ("gitdata-chat", "reaction-update")
31 | ("gitdata-chat", "room-update")
32 | ("gitdata-chat", "member-create")
33 | ("gitdata-chat", "member-delete")
34 | ("gitdata-chat", "thread-update")
35 | ("gitdata-chat", "voice-presence")
36 | ("gitdata-chat", "voice-hand")
37 | ("gitdata-chat", "voice-layout")
38 | ("gitdata-chat", "voice-moderate")
39 | ("gitdata-chat", "mention-create")
40 | ("gitdata-chat", "reminder-create")
41 | ("gitdata-chat", "membership-create")
42 | ("gitdata-chat", "read_state-update")
43 | ("gitdata-chat", "presence-update")
44 | ("gitdata-notify", "notification-create")
45 | ("gitdata-issue", "issue-create")
46 | ("gitdata-issue", "title-update")
47 | ("gitdata-issue", "milestone-update")
48 | ("gitdata-issue", "issue-close")
49 | ("gitdata-issue", "issue-reopen")
50 | ("gitdata-issue", "comment-create")
51 | ("gitdata-issue", "comment-update")
52 | ("gitdata-issue", "comment-delete")
53 | ("gitdata-issue", "member-create")
54 | ("gitdata-issue", "member-delete")
55 | ("gitdata-issue", "label-create")
56 | ("gitdata-issue", "label-delete")
57 | ("gitdata-issue", "dependency-create")
58 | ("gitdata-issue", "dependency-delete")
59 | ("gitdata-pull", "pull-create")
60 | ("gitdata-pull", "pull-update")
61 | ("gitdata-pull", "pull-close")
62 | ("gitdata-pull", "pull-reopen")
63 | ("gitdata-pull", "pull-merge")
64 | ("gitdata-pull", "comment-create")
65 | ("gitdata-pull", "review-create")
66 | ("gitdata-pull", "review-delete")
67 | ("gitdata-pull", "review-request-create")
68 | ("gitdata-pull", "review-request-delete")
69 | ("gitdata-pull", "head-update")
70 | ("gitdata-pull", "label-create")
71 | ("gitdata-pull", "label-delete")
72 | ("gitdata-pull", "assignee-create")
73 | ("gitdata-pull", "assignee-delete")
74 | ("gitdata-pull", "lock-create")
75 | ("gitdata-pull", "lock-delete")
76 )
77}
Added lib/events/src/gateway/action/kenshell.rs View lib/events/src/gateway/action/kenshell.rs
@@ -0,0 +1,65 @@
1use rocketmq_common::common::message::message_ext::MessageExt;
2
3use super::EventProperties;
4use super::common::{EventContext, deliver_to_subscribers, unimplemented};
5use crate::gateway::ConsumeResult;
6
7pub(super) async fn route(
8 topic: &str,
9 tag: &str,
10 message: MessageExt,
11 properties: EventProperties,
12 context: &EventContext<'_>,
13) -> ConsumeResult {
14 if !is_supported(topic, tag) {
15 return unimplemented(message);
16 }
17
18 deliver_to_subscribers(topic, tag, message, properties, context).await
19}
20
21pub(super) fn is_supported(topic: &str, tag: &str) -> bool {
22 matches!(
23 (topic, tag),
24 ("kenshell-repo", "create")
25 | ("kenshell-repo", "update")
26 | ("kenshell-repo", "delete")
27 | ("kenshell-repo", "placement-change")
28 | ("kenshell-repo", "language-update")
29 | ("kenshell-ref", "create-branch")
30 | ("kenshell-ref", "update-branch")
31 | ("kenshell-ref", "delete-branch")
32 | ("kenshell-ref", "rename-branch")
33 | ("kenshell-ref", "set-default-branch")
34 | ("kenshell-ref", "create-tag")
35 | ("kenshell-ref", "delete-tag")
36 | ("kenshell-ref", "create-protection")
37 | ("kenshell-ref", "delete-protection")
38 | ("kenshell-commit", "sync")
39 | ("kenshell-commit", "set-status")
40 | ("kenshell-commit", "comment-create")
41 | ("kenshell-commit", "comment-update")
42 | ("kenshell-commit", "comment-delete")
43 | ("kenshell-lfs", "upload")
44 | ("kenshell-lfs", "unreference")
45 | ("kenshell-lfs", "purge")
46 | ("kenshell-archive", "create")
47 | ("kenshell-job", "enqueue")
48 | ("kenshell-job", "claim")
49 | ("kenshell-job", "progress")
50 | ("kenshell-job", "heartbeat")
51 | ("kenshell-job", "cancel")
52 | ("kenshell-job", "retry")
53 | ("kenshell-job", "finish")
54 | ("kenshell-node", "create")
55 | ("kenshell-node", "update")
56 | ("kenshell-node", "delete")
57 | ("kenshell-node", "probe")
58 | ("kenshell-volume", "create")
59 | ("kenshell-volume", "update")
60 | ("kenshell-volume", "delete")
61 | ("kenshell-volume", "mount")
62 | ("kenshell-volume", "unmount")
63 | ("kenshell-volume", "probe")
64 )
65}
Added lib/events/src/gateway/action/mod.rs View lib/events/src/gateway/action/mod.rs
@@ -0,0 +1,49 @@
1mod common;
2mod gitdata;
3mod kenshell;
4mod platform_events;
5mod properties;
6
7pub use common::{EventContext, action};
8pub use properties::EventProperties;
9
10use serde_json::Value;
11
12use crate::gateway::Subscription;
13
14/// 与 MQ 扇出一致的下行 payload 形状(可选附带命中的订阅列表)。
15pub fn common_payload(
16 topic: &str,
17 tag: &str,
18 event_user_id: Option<i64>,
19 recipient_user_id: i64,
20 properties: &EventProperties,
21 body: &Value,
22 destinations: Option<Vec<Subscription>>,
23) -> Value {
24 let mut value = serde_json::json!({
25 "topic": topic,
26 "tag": tag,
27 "user_id": event_user_id,
28 "recipient_user_id": recipient_user_id,
29 "properties": properties,
30 "body": body,
31 });
32 if let Some(destinations) = destinations {
33 if let Ok(value_destinations) = serde_json::to_value(destinations) {
34 value["destinations"] = value_destinations;
35 }
36 }
37 value
38}
39
40/// topic/tag 是否属于本网关支持的事件族(平台定向投递与订阅校验共用)。
41pub fn is_supported(topic: &str, tag: &str) -> bool {
42 gitdata::is_supported(topic, tag)
43 || kenshell::is_supported(topic, tag)
44 || platform_events::is_supported(topic, tag)
45}
46
47pub fn is_personal_topic(topic: &str, tag: &str) -> bool {
48 crate::gateway::is_personal(topic, tag)
49}
Added lib/events/src/gateway/action/platform_events.rs View lib/events/src/gateway/action/platform_events.rs
@@ -0,0 +1,172 @@
1use futures_util::TryStreamExt;
2use rocketmq_common::common::message::message_ext::MessageExt;
3
4use super::EventProperties;
5use super::common::{EventContext, body, payload, retry, unimplemented, user_id};
6
7use crate::gateway::ConsumeResult;
8
9pub(crate) async fn route(
10 topic: &str,
11 tag: &str,
12 message: MessageExt,
13 properties: EventProperties,
14 context: &EventContext<'_>,
15) -> ConsumeResult {
16 if !is_supported(topic, tag) {
17 return unimplemented(message);
18 }
19
20 if topic == "gitdata-org" {
21 deliver_org(topic, tag, message, properties, context).await
22 } else {
23 deliver_user(topic, tag, message, properties, context).await
24 }
25}
26
27pub fn is_supported(topic: &str, tag: &str) -> bool {
28 matches!(
29 (topic, tag),
30 ("gitdata-auth", "login")
31 | ("gitdata-auth", "logout")
32 | ("gitdata-auth", "send-code")
33 | ("gitdata-auth", "register")
34 | ("gitdata-auth", "request-reset")
35 | ("gitdata-auth", "reset-password")
36 | ("gitdata-auth", "add-sshkey")
37 | ("gitdata-auth", "delete-sshkey")
38 | ("gitdata-auth", "create-accesskey")
39 | ("gitdata-auth", "rotate-accesskey")
40 | ("gitdata-auth", "delete-accesskey")
41 | ("gitdata-auth", "use-accesskey")
42 | ("gitdata-auth", "issue-token")
43 | ("gitdata-auth", "challenge")
44 | ("gitdata-auth", "verify-challenge")
45 | ("gitdata-auth", "delivery")
46 | ("gitdata-auth", "failure")
47 | ("gitdata-org", "create")
48 | ("gitdata-org", "update")
49 | ("gitdata-org", "archive")
50 | ("gitdata-org", "restore")
51 | ("gitdata-org", "delete")
52 | ("gitdata-org", "transfer-owner")
53 | ("gitdata-org", "invite")
54 | ("gitdata-org", "revoke-invite")
55 | ("gitdata-org", "redeem-invite")
56 | ("gitdata-org", "add-member")
57 | ("gitdata-org", "remove-member")
58 | ("gitdata-org", "update-member")
59 | ("gitdata-org", "set-role")
60 | ("gitdata-org", "ban")
61 | ("gitdata-org", "unban")
62 | ("gitdata-org", "update-ban")
63 | ("gitdata-org", "join")
64 | ("gitdata-org", "leave")
65 | ("gitdata-org", "create-role")
66 | ("gitdata-org", "set-permissions")
67 | ("gitdata-org", "update-role")
68 | ("gitdata-org", "delete-role")
69 | ("gitdata-org", "assign-role")
70 | ("gitdata-org", "unassign-role")
71 | ("gitdata-org", "grant")
72 )
73}
74
75async fn deliver_user(
76 topic: &str,
77 tag: &str,
78 message: MessageExt,
79 properties: EventProperties,
80 context: &EventContext<'_>,
81) -> ConsumeResult {
82 let Some(event_user_id) = user_id(&properties) else {
83 return ConsumeResult::Ack { message };
84 };
85
86 let body = body(&message);
87 let payload = payload(
88 topic,
89 tag,
90 Some(event_user_id),
91 event_user_id,
92 &properties,
93 &body,
94 );
95 match context.host.publish_event(event_user_id, payload).await {
96 Ok(()) => ConsumeResult::Ack { message },
97 Err(_) => retry(message),
98 }
99}
100
101async fn deliver_org(
102 topic: &str,
103 tag: &str,
104 message: MessageExt,
105 properties: EventProperties,
106 context: &EventContext<'_>,
107) -> ConsumeResult {
108 // 组织成员来源:KenApi space 域的 ListMembers RPC。
109 let Some(actor_user_id) = user_id(&properties) else {
110 return ConsumeResult::Ack { message };
111 };
112 let Some(organization_id) = organization_id(&properties) else {
113 return ConsumeResult::Ack { message };
114 };
115 let members = match context.platform.member_ids(organization_id).await {
116 Ok(members) if !members.is_empty() => members,
117 _ => return retry(message),
118 };
119 let body = body(&message);
120 let properties = &properties;
121 let body = &body;
122 let delivered = futures_util::stream::iter(members.into_iter().map(Ok::<_, ()>))
123 .try_for_each_concurrent(32, |member| async move {
124 let active = context
125 .platform
126 .is_member(member, organization_id)
127 .await
128 .map_err(|_| ())?;
129 let value = if active {
130 payload(topic, tag, Some(actor_user_id), member, properties, body)
131 } else if matches!(
132 tag,
133 "remove-member"
134 | "ban"
135 | "delete"
136 | "archive"
137 | "update-member"
138 | "leave"
139 | "set-role"
140 | "set-permissions"
141 | "unassign-role"
142 ) {
143 serde_json::json!({
144 "topic": "gitdata-org",
145 "tag": "access-revoked",
146 "recipient_user_id": member,
147 "properties": {"org_id": organization_id},
148 "body": {"id": body.get("id"), "kind": "organization.access.changed"},
149 "control": true,
150 })
151 } else {
152 return Ok(());
153 };
154 context
155 .host
156 .publish_event(member, value)
157 .await
158 .map_err(|_| ())
159 })
160 .await;
161 if delivered.is_err() {
162 return retry(message);
163 }
164 ConsumeResult::Ack { message }
165}
166
167fn organization_id(properties: &EventProperties) -> Option<&str> {
168 properties
169 .org_id
170 .as_deref()
171 .filter(|value| !value.is_empty())
172}
Added lib/events/src/gateway/action/properties.rs View lib/events/src/gateway/action/properties.rs
@@ -0,0 +1,104 @@
1use rocketmq_common::common::message::message_ext::MessageExt;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(default)]
6pub struct EventProperties {
7 pub user_id: Option<String>,
8 pub org_id: Option<String>,
9 pub request_id: Option<String>,
10 pub repo_id: Option<String>,
11 pub repo_name: Option<String>,
12 pub room_id: Option<String>,
13 pub message_id: Option<String>,
14 pub thread_id: Option<String>,
15 pub issue_id: Option<String>,
16 pub pull_id: Option<String>,
17 pub timestamp: Option<i64>,
18}
19
20impl EventProperties {
21 pub fn normalize(&mut self) -> Result<(), String> {
22 for id in [
23 &mut self.org_id,
24 &mut self.repo_id,
25 &mut self.room_id,
26 &mut self.message_id,
27 &mut self.thread_id,
28 &mut self.issue_id,
29 &mut self.pull_id,
30 ]
31 .into_iter()
32 .flatten()
33 {
34 *id = uuid::Uuid::parse_str(id)
35 .map_err(|_| "resource IDs must be UUIDs")?
36 .to_string();
37 }
38 if let Some(user) = &mut self.user_id {
39 let id = user.parse::<i64>().map_err(|_| "invalid user id")?;
40 if id <= 0 {
41 return Err("user id must be positive".to_string());
42 }
43 *user = id.to_string();
44 }
45 Ok(())
46 }
47 pub(super) fn from_message(message: &MessageExt) -> Self {
48 let properties = message.properties();
49 Self {
50 user_id: properties.get("user").map(ToString::to_string),
51 org_id: properties.get("org").map(ToString::to_string),
52 request_id: properties.get("request_id").map(ToString::to_string),
53 repo_id: properties.get("repo_id").map(ToString::to_string),
54 repo_name: properties.get("repo_name").map(ToString::to_string),
55 room_id: properties.get("room_id").map(ToString::to_string),
56 message_id: properties.get("message_id").map(ToString::to_string),
57 thread_id: properties.get("thread_id").map(ToString::to_string),
58 issue_id: properties.get("issue_id").map(ToString::to_string),
59 pull_id: properties.get("pull_id").map(ToString::to_string),
60 timestamp: properties
61 .get("timestamp")
62 .and_then(|value| value.as_str().parse().ok()),
63 }
64 }
65
66 pub fn is_empty(&self) -> bool {
67 self.user_id.is_none()
68 && self.org_id.is_none()
69 && self.request_id.is_none()
70 && self.repo_id.is_none()
71 && self.repo_name.is_none()
72 && self.room_id.is_none()
73 && self.message_id.is_none()
74 && self.thread_id.is_none()
75 && self.issue_id.is_none()
76 && self.pull_id.is_none()
77 && self.timestamp.is_none()
78 }
79
80 pub fn matches(&self, event: &Self) -> bool {
81 if self.is_empty() {
82 return false;
83 }
84
85 matches_field(&self.user_id, &event.user_id)
86 && matches_field(&self.org_id, &event.org_id)
87 && matches_field(&self.request_id, &event.request_id)
88 && matches_field(&self.repo_id, &event.repo_id)
89 && matches_field(&self.repo_name, &event.repo_name)
90 && matches_field(&self.room_id, &event.room_id)
91 && matches_field(&self.message_id, &event.message_id)
92 && matches_field(&self.thread_id, &event.thread_id)
93 && matches_field(&self.issue_id, &event.issue_id)
94 && matches_field(&self.pull_id, &event.pull_id)
95 && matches_field(&self.timestamp, &event.timestamp)
96 }
97}
98
99fn matches_field<T: PartialEq>(filter: &Option<T>, value: &Option<T>) -> bool {
100 match filter {
101 Some(expected) => value.as_ref() == Some(expected),
102 None => true,
103 }
104}
Added lib/events/src/gateway/event_service.rs View lib/events/src/gateway/event_service.rs
@@ -0,0 +1,224 @@
1use std::sync::Arc;
2
3use actor::ActorHost;
4use internal::envelope::{EnvelopeVerifier, VerifiedIdentity};
5use socket::SocketHub;
6
7use crate::gateway::action::EventProperties;
8use crate::gateway::platform::PlatformClient;
9use crate::gateway::subscriptions::{Subscription, SubscriptionRequest, Subscriptions};
10use crate::gateway::{ConsumeResult, is_personal};
11
12/// 事件域唯一业务入口:socket 路径与内部 RPC 路径共用同一套订阅/授权/投递逻辑。
13#[derive(Clone)]
14pub struct EventService {
15 host: ActorHost,
16 platform: PlatformClient,
17 subscriptions: Subscriptions,
18 verifier: Arc<EnvelopeVerifier>,
19 hub: SocketHub,
20}
21
22impl EventService {
23 pub fn new(
24 host: ActorHost,
25 platform: PlatformClient,
26 subscriptions: Subscriptions,
27 verifier: Arc<EnvelopeVerifier>,
28 hub: SocketHub,
29 ) -> Self {
30 Self {
31 host,
32 platform,
33 subscriptions,
34 verifier,
35 hub,
36 }
37 }
38
39 pub fn host(&self) -> &ActorHost {
40 &self.host
41 }
42
43 pub fn platform(&self) -> &PlatformClient {
44 &self.platform
45 }
46
47 pub fn subscriptions(&self) -> &Subscriptions {
48 &self.subscriptions
49 }
50
51 /// 解密身份信封 + 平台权威复核。
52 pub async fn verify_identity(
53 &self,
54 envelope: &proto::internal::v1::IdentityEnvelope,
55 ) -> Result<VerifiedIdentity, tonic::Status> {
56 let identity = self.verifier.verify(envelope).await?;
57 self.platform
58 .attest(
59 identity.user_id,
60 identity.security_version,
61 &identity.request_id,
62 )
63 .await
64 .map_err(|error| error.into_status())?;
65 Ok(identity)
66 }
67
68 /// 订阅(浏览器 socket 与内部 RPC 共用)。
69 pub async fn subscribe(
70 &self,
71 user_id: i64,
72 device_id: &str,
73 namespace: &str,
74 request: SubscriptionRequest,
75 access_token: &str,
76 ) -> Result<String, String> {
77 self.subscriptions
78 .subscribe(
79 &self.platform,
80 user_id,
81 device_id,
82 namespace,
83 request,
84 access_token,
85 )
86 .await
87 }
88
89 pub async fn unsubscribe(
90 &self,
91 user_id: i64,
92 device_id: &str,
93 namespace: &str,
94 topic: &str,
95 tag: &str,
96 id: &str,
97 ) -> Result<(), String> {
98 self.subscriptions
99 .unsubscribe(user_id, device_id, namespace, topic, tag, id)
100 .await
101 }
102
103 pub async fn list(
104 &self,
105 user_id: i64,
106 device_id: &str,
107 namespace: &str,
108 ) -> Result<Vec<Subscription>, String> {
109 self.subscriptions.list(user_id, device_id, namespace).await
110 }
111
112 /// 平台定向投递:身份已校验,需再过订阅匹配 + 投递期授权。
113 pub async fn publish(
114 &self,
115 identity: &VerifiedIdentity,
116 topic: &str,
117 tag: &str,
118 properties: EventProperties,
119 body: serde_json::Value,
120 personal: bool,
121 request_id: &str,
122 ) -> Result<u32, tonic::Status> {
123 if !crate::gateway::action::is_supported(topic, tag) {
124 return Err(tonic::Status::invalid_argument("unsupported topic or tag"));
125 }
126 let personal = personal || is_personal(topic, tag);
127 let recipient = identity.user_id;
128 if personal {
129 if properties
130 .user_id
131 .as_deref()
132 .is_some_and(|value| value != recipient.to_string())
133 {
134 return Err(tonic::Status::permission_denied(
135 "personal events must target the identity user",
136 ));
137 }
138 let payload = crate::gateway::action::common_payload(
139 topic,
140 tag,
141 Some(recipient),
142 recipient,
143 &properties,
144 &body,
145 None,
146 );
147 self.host
148 .publish_event(recipient, payload)
149 .await
150 .map_err(|_| tonic::Status::unavailable("delivery store is unavailable"))?;
151 return Ok(1);
152 }
153 let subscribers = self
154 .subscriptions
155 .subscribers(topic, tag, &properties)
156 .await
157 .map_err(|_| tonic::Status::failed_precondition("subscription store is unavailable"))?;
158 let mut delivered = 0;
159 for subscription in subscribers
160 .into_iter()
161 .filter(|subscription| subscription.user_id == recipient)
162 .filter(|subscription| subscription.filter.matches(&properties))
163 {
164 let allowed = self
165 .platform
166 .authorize(recipient, &subscription.filter, "DELIVER")
167 .await
168 .map_err(|error| error.into_status())?;
169 if !allowed {
170 continue;
171 }
172 if let Some(org) = subscription.filter.org_id.as_deref() {
173 if !self
174 .platform
175 .is_member(recipient, org)
176 .await
177 .map_err(|error| error.into_status())?
178 {
179 continue;
180 }
181 }
182 let payload = crate::gateway::action::common_payload(
183 topic,
184 tag,
185 properties
186 .user_id
187 .as_deref()
188 .and_then(|value| value.parse::<i64>().ok()),
189 recipient,
190 &properties,
191 &body,
192 Some(vec![subscription.clone()]),
193 );
194 if self.host.publish_event(recipient, payload).await.is_ok() {
195 delivered += 1;
196 }
197 }
198 let _ = request_id;
199 Ok(delivered)
200 }
201
202 /// 撤销连接/权限:发控制事件并关闭该用户在线会话。
203 pub async fn revoke(
204 &self,
205 identity: &VerifiedIdentity,
206 reason: &str,
207 ) -> Result<u32, tonic::Status> {
208 let payload = serde_json::json!({
209 "topic": "event",
210 "tag": "access-revoked",
211 "control": true,
212 "recipient_user_id": identity.user_id,
213 "properties": {"user_id": identity.user_id},
214 "body": {"reason": reason},
215 });
216 self.host
217 .publish_event(identity.user_id, payload)
218 .await
219 .map_err(|_| tonic::Status::unavailable("delivery store is unavailable"))?;
220 Ok(self.hub.close_user(identity.user_id).await as u32)
221 }
222
223 pub fn consume_result(_: ConsumeResult) {}
224}
Added lib/events/src/gateway/listener.rs View lib/events/src/gateway/listener.rs
@@ -0,0 +1,51 @@
1use rocketmq_client_rust::consumer::listener::consume_orderly_context::ConsumeOrderlyContext;
2use rocketmq_client_rust::consumer::listener::consume_orderly_status::ConsumeOrderlyStatus;
3use rocketmq_client_rust::consumer::listener::message_listener_orderly::MessageListenerOrderly;
4use rocketmq_common::RocketMQResult;
5use rocketmq_common::common::message::message_ext::MessageExt;
6use std::sync::mpsc::{SyncSender, sync_channel};
7use std::time::{Duration, Instant};
8use tokio::sync::mpsc;
9
10pub(crate) struct PendingEvent {
11 pub(crate) message: MessageExt,
12 pub(crate) completion: SyncSender<bool>,
13}
14
15pub(crate) struct Forwarder {
16 events: mpsc::Sender<PendingEvent>,
17 timeout: Duration,
18}
19
20impl Forwarder {
21 pub(crate) fn new(events: mpsc::Sender<PendingEvent>, timeout: Duration) -> Self {
22 Self { events, timeout }
23 }
24}
25
26impl MessageListenerOrderly for Forwarder {
27 fn consume_message(
28 &self,
29 msgs: &[&MessageExt],
30 _context: &mut ConsumeOrderlyContext,
31 ) -> RocketMQResult<ConsumeOrderlyStatus> {
32 let deadline = Instant::now() + self.timeout;
33 for message in msgs {
34 let (completion, result) = sync_channel(1);
35 if self
36 .events
37 .try_send(PendingEvent {
38 message: (*message).clone(),
39 completion,
40 })
41 .is_err()
42 {
43 return Ok(ConsumeOrderlyStatus::SuspendCurrentQueueAMoment);
44 }
45 if result.recv_timeout(deadline.saturating_duration_since(Instant::now())) != Ok(true) {
46 return Ok(ConsumeOrderlyStatus::SuspendCurrentQueueAMoment);
47 }
48 }
49 Ok(ConsumeOrderlyStatus::Success)
50 }
51}
Added lib/events/src/gateway/mod.rs View lib/events/src/gateway/mod.rs
@@ -0,0 +1,20 @@
1//! 实时事件网关:RocketMQ 消费、订阅索引、投递编排与内部 RPC 业务入口。
2//!
3//! 业务边界:本模块只做事件路由/订阅/投递与身份校验编排,
4//! 权限判定统一走平台内部 RPC(`internal.v1.*AuthorizationService`)。
5
6pub mod action;
7pub mod event_service;
8pub mod listener;
9pub mod pipe;
10pub mod plan;
11pub mod platform;
12pub mod result;
13pub mod socket_backend;
14pub mod socket_runtime;
15pub mod subscriptions;
16
17pub use pipe::{EventsPipe, QUEUE_CAPACITY};
18pub use result::ConsumeResult;
19pub use socket::EmitPacket;
20pub use subscriptions::{Subscription, SubscriptionRequest, Subscriptions, is_personal};
Added lib/events/src/gateway/pipe.rs View lib/events/src/gateway/pipe.rs
@@ -0,0 +1,118 @@
1use rocketmq_client_rust::consumer::default_mq_push_consumer::DefaultMQPushConsumer;
2use rocketmq_client_rust::consumer::mq_push_consumer::MQPushConsumer;
3use rocketmq_common::common::message::message_ext::MessageExt;
4use rocketmq_common::{RocketMQError, RocketMQResult};
5use std::collections::{HashMap, VecDeque};
6use std::sync::mpsc::SyncSender;
7use std::time::Duration;
8use tokio::sync::mpsc;
9
10use crate::gateway::plan;
11use config::event::consumer::RocketMqConsumerConfig;
12
13use super::listener::{Forwarder, PendingEvent};
14use super::result::ConsumeResult;
15
16pub const QUEUE_CAPACITY: usize = 1024;
17
18pub struct EventsPipe {
19 consumer: DefaultMQPushConsumer,
20 events_rx: mpsc::Receiver<PendingEvent>,
21 pending: HashMap<String, VecDeque<SyncSender<bool>>>,
22 prefixes: Vec<String>,
23 result_tx: mpsc::Sender<ConsumeResult>,
24 result_rx: mpsc::Receiver<ConsumeResult>,
25}
26
27impl EventsPipe {
28 pub fn new(config: &RocketMqConsumerConfig) -> Result<Self, String> {
29 config.validate()?;
30
31 let (events_tx, events_rx) = mpsc::channel(QUEUE_CAPACITY);
32 let (result_tx, result_rx) = mpsc::channel(QUEUE_CAPACITY);
33
34 let mut consumer = DefaultMQPushConsumer::builder()
35 .consumer_group(config.consumer_group.clone())
36 .name_server_addr(config.name_servers().join(";"))
37 .consume_timeout(config.consume_timeout_minutes())
38 .max_reconsume_times(config.max_reconsume_times as i32)
39 .pull_batch_size(config.pull_batch_size)
40 .pull_batch_size_in_bytes(config.pull_batch_size_in_bytes)
41 .pull_interval(config.pull_interval_ms)
42 .build();
43 consumer.register_message_listener_orderly(Forwarder::new(
44 events_tx,
45 Duration::from_millis(config.consume_timeout_ms),
46 ));
47
48 Ok(Self {
49 consumer,
50 events_rx,
51 pending: HashMap::new(),
52 prefixes: config.topic_prefixes.clone(),
53 result_tx,
54 result_rx,
55 })
56 }
57
58 pub async fn next_event(&mut self) -> Option<MessageExt> {
59 let pending = self.events_rx.recv().await?;
60 self.pending
61 .entry(pending.message.msg_id().to_string())
62 .or_default()
63 .push_back(pending.completion);
64 Some(pending.message)
65 }
66
67 pub fn result_sender(&self) -> mpsc::Sender<ConsumeResult> {
68 self.result_tx.clone()
69 }
70
71 pub async fn next_result(&mut self) -> Option<ConsumeResult> {
72 self.result_rx.recv().await
73 }
74
75 pub async fn apply_result(&mut self, result: ConsumeResult) -> RocketMQResult<()> {
76 let (message, success) = match result {
77 ConsumeResult::Ack { message } => (message, true),
78 ConsumeResult::Retry { message, .. } => (message, false),
79 };
80 let key = message.msg_id().to_string();
81 if let Some(queue) = self.pending.get_mut(&key) {
82 if let Some(completion) = queue.pop_front() {
83 let _ = completion.send(success);
84 }
85 if queue.is_empty() {
86 self.pending.remove(&key);
87 }
88 }
89 Ok(())
90 }
91
92 pub async fn subscribe(&mut self, topic: &str, expression: &str) -> RocketMQResult<()> {
93 self.consumer.subscribe(topic, expression).await
94 }
95
96 pub async fn subscribe_all(&mut self) -> RocketMQResult<()> {
97 let plan = plan::plan(&self.prefixes).map_err(|error| {
98 RocketMQError::illegal_argument(format!("invalid docs/events.json: {error}"))
99 })?;
100
101 for (topic, expression) in plan {
102 self.consumer.subscribe(topic, expression).await?;
103 }
104
105 Ok(())
106 }
107
108 pub async fn start(&mut self) -> RocketMQResult<()> {
109 self.consumer.start().await
110 }
111
112 pub async fn shutdown(&mut self) {
113 self.events_rx.close();
114 while self.events_rx.try_recv().is_ok() {}
115 self.pending.clear();
116 self.consumer.shutdown().await;
117 }
118}
Added lib/events/src/gateway/plan.rs View lib/events/src/gateway/plan.rs
@@ -0,0 +1,37 @@
1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4
5#[derive(Deserialize)]
6pub struct EventDocs {
7 events: Vec<EventDoc>,
8}
9
10#[derive(Deserialize)]
11pub struct EventDoc {
12 topic: String,
13 tag: String,
14}
15
16pub fn plan(prefixes: &[String]) -> Result<BTreeMap<String, String>, serde_json::Error> {
17 let docs: EventDocs = serde_json::from_str(include_str!("../../data/events.json"))?;
18
19 let mut subscriptions: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
20 for event in &docs.events {
21 if !prefixes
22 .iter()
23 .any(|prefix| event.topic.starts_with(&format!("{prefix}-")))
24 {
25 continue;
26 }
27 let tags = subscriptions.entry(event.topic.as_str()).or_default();
28 if !tags.contains(&event.tag.as_str()) {
29 tags.push(event.tag.as_str());
30 }
31 }
32
33 Ok(subscriptions
34 .into_iter()
35 .map(|(topic, tags)| (topic.to_string(), tags.join(" || ")))
36 .collect())
37}
Added lib/events/src/gateway/platform.rs View lib/events/src/gateway/platform.rs
@@ -0,0 +1,336 @@
1use std::fmt;
2use std::time::Duration;
3
4use config::event::platform::PlatformClientConfig;
5use internal::Connection;
6use proto::internal::v1::identity_service_client::IdentityServiceClient;
7use proto::internal::v1::{
8 AttestIdentityRequest, CredentialKind, PublishRequest, ResolveIdentityRequest,
9};
10
11use crate::gateway::action::EventProperties;
12
13#[derive(Debug, Clone)]
14pub enum PlatformError {
15 Config(String),
16 Rejected(String),
17 Unavailable(String),
18}
19
20impl fmt::Display for PlatformError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::Config(message) => write!(f, "config: {message}"),
24 Self::Rejected(message) => write!(f, "rejected: {message}"),
25 Self::Unavailable(message) => write!(f, "unavailable: {message}"),
26 }
27 }
28}
29
30impl std::error::Error for PlatformError {}
31
32impl PlatformError {
33 pub fn into_status(self) -> tonic::Status {
34 match self {
35 Self::Config(message) => tonic::Status::failed_precondition(message),
36 Self::Rejected(message) => tonic::Status::permission_denied(message),
37 Self::Unavailable(_) => tonic::Status::unavailable("platform service is unavailable"),
38 }
39 }
40}
41
42impl From<tonic::Status> for PlatformError {
43 fn from(status: tonic::Status) -> Self {
44 match status.code() {
45 tonic::Code::Unauthenticated
46 | tonic::Code::PermissionDenied
47 | tonic::Code::InvalidArgument => Self::Rejected(status.message().to_string()),
48 _ => Self::Unavailable(status.message().to_string()),
49 }
50 }
51}
52
53/// 事件进程对基础核心平台的唯一出口。
54#[derive(Clone)]
55pub struct PlatformClient {
56 rpc: Connection,
57 http: reqwest::Client,
58 resource_api_endpoint: String,
59 resource_service_key: String,
60 timeout: Duration,
61}
62
63impl PlatformClient {
64 pub fn new(config: &PlatformClientConfig) -> Result<Self, PlatformError> {
65 config.validate().map_err(PlatformError::Config)?;
66 let timeout = Duration::from_secs(config.timeout_secs.max(1));
67 let rpc = Connection::connect(&config.rpc_endpoint, &config.rpc_key, timeout)
68 .map_err(|error| PlatformError::Config(error.to_string()))?;
69 let http = reqwest::Client::builder()
70 .timeout(timeout)
71 .build()
72 .map_err(|error| PlatformError::Config(error.to_string()))?;
73 Ok(Self {
74 rpc,
75 http,
76 resource_api_endpoint: config
77 .resource_api_endpoint
78 .trim_end_matches('/')
79 .to_string(),
80 resource_service_key: config.resource_api_service_key.trim().to_string(),
81 timeout,
82 })
83 }
84
85 pub fn rpc(&self) -> &Connection {
86 &self.rpc
87 }
88
89 /// KenApi 会话 cookie 的值 → user_id。
90 pub async fn authenticate(&self, credential: &str) -> Result<i64, PlatformError> {
91 if credential.is_empty() {
92 return Err(PlatformError::Rejected(
93 "session credential is empty".to_string(),
94 ));
95 }
96 let mut client =
97 IdentityServiceClient::with_interceptor(self.rpc.channel(), self.rpc.interceptor());
98 let request = tonic::Request::new(ResolveIdentityRequest {
99 kind: CredentialKind::Session as i32,
100 credential: credential.to_string(),
101 required_scopes: vec!["event".to_string()],
102 request_id: String::new(),
103 });
104 let response = tokio::time::timeout(self.timeout, client.resolve(request))
105 .await
106 .map_err(|_| PlatformError::Unavailable("identity resolve timed out".to_string()))??;
107 response
108 .into_inner()
109 .principal
110 .map(|principal| principal.user_id)
111 .ok_or_else(|| PlatformError::Rejected("identity has no principal".to_string()))
112 }
113
114 /// 信封身份的权威复核(用户仍有效、安全版本一致)。
115 pub async fn attest(
116 &self,
117 user_id: i64,
118 security_version: i64,
119 request_id: &str,
120 ) -> Result<(), PlatformError> {
121 let mut client =
122 IdentityServiceClient::with_interceptor(self.rpc.channel(), self.rpc.interceptor());
123 let request = tonic::Request::new(AttestIdentityRequest {
124 user_id,
125 security_version,
126 request_id: request_id.to_string(),
127 });
128 let response = tokio::time::timeout(self.timeout, client.attest(request))
129 .await
130 .map_err(|_| PlatformError::Unavailable("identity attest timed out".to_string()))??;
131 let response = response.into_inner();
132 if response.active {
133 Ok(())
134 } else {
135 Err(PlatformError::Rejected(
136 "identity is no longer active".to_string(),
137 ))
138 }
139 }
140
141 /// 资源订阅/投递授权:按 filter 命中哪个域,就调哪个域的授权 RPC。
142 pub async fn authorize(
143 &self,
144 user_id: i64,
145 filter: &EventProperties,
146 action: &str,
147 ) -> Result<bool, PlatformError> {
148 if let Some(room_id) = filter.room_id.as_deref() {
149 let mut client = proto::internal::v1::chat_authorization_service_client::ChatAuthorizationServiceClient::with_interceptor(
150 self.rpc.channel(),
151 self.rpc.interceptor(),
152 );
153 let request = tonic::Request::new(proto::internal::v1::AuthorizeRoomRequest {
154 user_id,
155 room_id: room_id.to_string(),
156 action: action.to_string(),
157 request_id: String::new(),
158 });
159 return Ok(
160 tokio::time::timeout(self.timeout, client.authorize_room(request))
161 .await
162 .map_err(|_| {
163 PlatformError::Unavailable("room authorization timed out".to_string())
164 })??
165 .into_inner()
166 .allowed,
167 );
168 }
169 if let Some(issue_id) = filter.issue_id.as_deref() {
170 let mut client = proto::internal::v1::issue_authorization_service_client::IssueAuthorizationServiceClient::with_interceptor(
171 self.rpc.channel(),
172 self.rpc.interceptor(),
173 );
174 let request = tonic::Request::new(proto::internal::v1::AuthorizeIssueRequest {
175 user_id,
176 issue_id: issue_id.to_string(),
177 action: action.to_string(),
178 request_id: String::new(),
179 });
180 return Ok(
181 tokio::time::timeout(self.timeout, client.authorize_issue(request))
182 .await
183 .map_err(|_| {
184 PlatformError::Unavailable("issue authorization timed out".to_string())
185 })??
186 .into_inner()
187 .allowed,
188 );
189 }
190 if let Some(pull_id) = filter.pull_id.as_deref() {
191 let mut client = proto::internal::v1::pull_authorization_service_client::PullAuthorizationServiceClient::with_interceptor(
192 self.rpc.channel(),
193 self.rpc.interceptor(),
194 );
195 let request = tonic::Request::new(proto::internal::v1::AuthorizePullRequest {
196 user_id,
197 pull_id: pull_id.to_string(),
198 action: action.to_string(),
199 request_id: String::new(),
200 });
201 return Ok(
202 tokio::time::timeout(self.timeout, client.authorize_pull(request))
203 .await
204 .map_err(|_| {
205 PlatformError::Unavailable("pull authorization timed out".to_string())
206 })??
207 .into_inner()
208 .allowed,
209 );
210 }
211 if let Some(repo_id) = filter.repo_id.as_deref() {
212 let mut client = proto::internal::v1::space_authorization_service_client::SpaceAuthorizationServiceClient::with_interceptor(
213 self.rpc.channel(),
214 self.rpc.interceptor(),
215 );
216 let request = tonic::Request::new(proto::internal::v1::AuthorizeResourceRequest {
217 user_id,
218 repo_id: repo_id.to_string(),
219 space_id: String::new(),
220 action: action.to_string(),
221 request_id: String::new(),
222 });
223 return Ok(
224 tokio::time::timeout(self.timeout, client.authorize_repo(request))
225 .await
226 .map_err(|_| {
227 PlatformError::Unavailable("repo authorization timed out".to_string())
228 })??
229 .into_inner()
230 .allowed,
231 );
232 }
233 // 没有资源维度的订阅(personal / cross-user)由调用方各自校验。
234 Ok(true)
235 }
236
237 /// 组织成员校验:组织数据在 KenApi 的 space 域(space id == org id)。
238 pub async fn is_member(
239 &self,
240 user_id: i64,
241 organization_id: &str,
242 ) -> Result<bool, PlatformError> {
243 let mut client = proto::internal::v1::space_authorization_service_client::SpaceAuthorizationServiceClient::with_interceptor(
244 self.rpc.channel(),
245 self.rpc.interceptor(),
246 );
247 let request = tonic::Request::new(proto::internal::v1::AuthorizeResourceRequest {
248 user_id,
249 repo_id: String::new(),
250 space_id: organization_id.to_string(),
251 action: "DELIVER".to_string(),
252 request_id: String::new(),
253 });
254 Ok(
255 tokio::time::timeout(self.timeout, client.authorize_space(request))
256 .await
257 .map_err(|_| {
258 PlatformError::Unavailable("organization authorization timed out".to_string())
259 })??
260 .into_inner()
261 .allowed,
262 )
263 }
264
265 /// 组织成员 id(事件扇出用):组织数据在 KenApi space 域。
266 pub async fn member_ids(&self, organization_id: &str) -> Result<Vec<i64>, PlatformError> {
267 let mut client = proto::internal::v1::space_authorization_service_client::SpaceAuthorizationServiceClient::with_interceptor(
268 self.rpc.channel(),
269 self.rpc.interceptor(),
270 );
271 let request = tonic::Request::new(proto::internal::v1::SpaceMembersRequest {
272 space_id: organization_id.to_string(),
273 request_id: String::new(),
274 });
275 Ok(
276 tokio::time::timeout(self.timeout, client.list_members(request))
277 .await
278 .map_err(|_| {
279 PlatformError::Unavailable("organization members timed out".to_string())
280 })??
281 .into_inner()
282 .user_ids,
283 )
284 }
285
286 /// 上报聊天连接生命周期(平台据此维护在线态、抑制 Web Push)。
287 pub async fn report_connection(
288 &self,
289 phase: &str,
290 user_id: i64,
291 session: &str,
292 ) -> Result<(), PlatformError> {
293 if self.resource_service_key.is_empty() {
294 return Ok(());
295 }
296 let path = match phase {
297 "open" => "/internal/chat/connections",
298 "heartbeat" => "/internal/chat/connections/heartbeat",
299 "close" => "/internal/chat/connections/close",
300 _ => return Ok(()),
301 };
302 let response = self
303 .http
304 .post(format!("{}{path}", self.resource_api_endpoint))
305 .header("content-type", "application/json")
306 .header("x-gitdata-service-key", &self.resource_service_key)
307 .json(&serde_json::json!({"user_id": user_id, "session_id": session}))
308 .send()
309 .await
310 .map_err(|error| PlatformError::Unavailable(error.to_string()))?;
311 if response.status().is_success() {
312 Ok(())
313 } else {
314 Err(PlatformError::Unavailable(format!(
315 "chat connection report returned {}",
316 response.status()
317 )))
318 }
319 }
320
321 pub fn publish_client(
322 &self,
323 ) -> proto::internal::v1::event_ingress_service_client::EventIngressServiceClient<
324 tonic::service::interceptor::InterceptedService<
325 tonic::transport::Channel,
326 internal::RpcInterceptor,
327 >,
328 > {
329 proto::internal::v1::event_ingress_service_client::EventIngressServiceClient::with_interceptor(
330 self.rpc.channel(),
331 self.rpc.interceptor(),
332 )
333 }
334
335 pub fn _unused_publish(_: PublishRequest) {}
336}
Added lib/events/src/gateway/result.rs View lib/events/src/gateway/result.rs
@@ -0,0 +1,11 @@
1use rocketmq_common::common::message::message_ext::MessageExt;
2
3pub enum ConsumeResult {
4 Ack {
5 message: MessageExt,
6 },
7 Retry {
8 message: MessageExt,
9 delay_level: i32,
10 },
11}
Added lib/events/src/gateway/socket_backend.rs View lib/events/src/gateway/socket_backend.rs
@@ -0,0 +1,255 @@
1use std::time::Duration;
2
3use actor::{ActorHost, PushStore};
4use async_trait::async_trait;
5use socket::{
6 ConnectedSession, ConnectionPhase, DeliveryContext, SocketBackend, SocketError, SocketPayload,
7};
8
9use crate::gateway::platform::{PlatformClient, PlatformError};
10use crate::gateway::subscriptions::Subscriptions;
11
12/// socket 通道与事件业务之间的适配层:身份、订阅、投递策略都落在这里。
13#[derive(Clone)]
14pub struct RealtimeBackend {
15 host: ActorHost,
16 platform: PlatformClient,
17 subscriptions: Subscriptions,
18 push_store: PushStore,
19}
20
21impl RealtimeBackend {
22 pub fn new(
23 host: ActorHost,
24 platform: PlatformClient,
25 subscriptions: Subscriptions,
26 push_store: PushStore,
27 ) -> Self {
28 Self {
29 host,
30 platform,
31 subscriptions,
32 push_store,
33 }
34 }
35}
36
37#[async_trait]
38impl SocketBackend for RealtimeBackend {
39 async fn authenticate(&self, token: &str) -> Result<i64, SocketError> {
40 self.platform
41 .authenticate(token)
42 .await
43 .map_err(|error| match error {
44 PlatformError::Rejected(message) => SocketError::Rejected(message),
45 PlatformError::Unavailable(message) => SocketError::Unavailable(message),
46 PlatformError::Config(message) => SocketError::Config(message),
47 })
48 }
49
50 async fn on_connect(&self, session: &ConnectedSession) -> Result<(), SocketError> {
51 self.push_store
52 .register_device(session.user_id, &session.device_id, &session.namespace)
53 .await
54 .map_err(SocketError::Unavailable)?;
55 if session.first_namespace {
56 self.host
57 .socket_heartbeat(session.user_id)
58 .await
59 .map_err(|error| SocketError::Unavailable(error.to_string()))?;
60 }
61 Ok(())
62 }
63
64 async fn heartbeat(&self, user_id: i64) {
65 let _ = self.host.socket_heartbeat(user_id).await;
66 }
67
68 async fn report_connection(&self, phase: ConnectionPhase, user_id: i64, session: &str) {
69 let phase = match phase {
70 ConnectionPhase::Open => "open",
71 ConnectionPhase::Heartbeat => "heartbeat",
72 ConnectionPhase::Close => "close",
73 };
74 if let Err(error) = self
75 .platform
76 .report_connection(phase, user_id, session)
77 .await
78 {
79 tracing::warn!(%error, "chat connection report failed");
80 }
81 }
82
83 async fn run_delivery(&self, context: DeliveryContext) {
84 let store = self.push_store.clone();
85 let subscriptions = self.subscriptions.clone();
86 let platform = self.platform.clone();
87 let hub = context.hub.clone();
88 let input = context.handle.clone();
89 let user = context.user_id;
90 let device = context.device_id.clone();
91 let namespace = context.namespace.clone();
92 let token = context.access_token.clone();
93 let owner = format!("{}:{}", context.session_sid, context.session_id);
94 let mut stopped = context.stop;
95 let mut shutdown = hub.shutdown_receiver();
96 let notification = hub.notification(user).await;
97 let work = async {
98 let mut verified: Option<tokio::time::Instant> = None;
99 loop {
100 let changed = notification.notified();
101 tokio::pin!(changed);
102 changed.as_mut().enable();
103 if verified.is_none_or(|verified: tokio::time::Instant| {
104 verified.elapsed() >= Duration::from_secs(60)
105 }) {
106 match platform.authenticate(&token).await {
107 Ok(current) if current == user => {
108 verified = Some(tokio::time::Instant::now())
109 }
110 Err(PlatformError::Unavailable(_)) => {
111 tokio::time::sleep(Duration::from_secs(1)).await;
112 continue;
113 }
114 _ => {
115 input.close().await;
116 break;
117 }
118 }
119 }
120 if !matches!(
121 store.claim(user, &device, &namespace, &owner).await,
122 Ok(true)
123 ) {
124 tokio::time::sleep(Duration::from_secs(1)).await;
125 continue;
126 }
127 let (cursor, next) = match store.next(user, &device, &namespace).await {
128 Ok(next) => next,
129 Err(_) => {
130 tokio::time::sleep(Duration::from_secs(1)).await;
131 continue;
132 }
133 };
134 let Some((delivery, mut payload)) = next else {
135 tokio::select! {
136 _ = &mut changed => {}
137 _ = tokio::time::sleep(Duration::from_secs(1)) => {}
138 }
139 continue;
140 };
141 let authorization = async {
142 if let Some(destinations) = payload.get("destinations") {
143 let destinations: Vec<crate::gateway::Subscription> =
144 serde_json::from_value(destinations.clone())
145 .map_err(|error| PlatformError::Unavailable(error.to_string()))?;
146 for subscription in destinations {
147 if subscription.device_id != device
148 || subscription.namespace != namespace
149 {
150 continue;
151 }
152 if !subscriptions
153 .contains(&subscription)
154 .await
155 .map_err(PlatformError::Unavailable)?
156 {
157 continue;
158 }
159 if !platform
160 .authorize(user, &subscription.filter, "DELIVER")
161 .await?
162 {
163 continue;
164 }
165 if let Some(org) = subscription.filter.org_id.as_deref() {
166 if !platform.is_member(user, org).await? {
167 continue;
168 }
169 }
170 return Ok(true);
171 }
172 return Ok(false);
173 }
174 if payload.get("source").and_then(serde_json::Value::as_str) == Some("socket") {
175 return Ok(payload.get("namespace").and_then(serde_json::Value::as_str)
176 == Some(namespace.as_str()));
177 }
178 if payload.get("control").and_then(serde_json::Value::as_bool) != Some(true) {
179 if let Some(org) = payload
180 .pointer("/properties/org_id")
181 .and_then(serde_json::Value::as_str)
182 {
183 return platform.is_member(user, org).await;
184 }
185 }
186 Ok(true)
187 }
188 .await;
189 let allowed = match authorization {
190 Ok(allowed) => allowed,
191 Err(PlatformError::Rejected(_)) => {
192 input.close().await;
193 break;
194 }
195 Err(_) => {
196 tokio::time::sleep(Duration::from_secs(1)).await;
197 continue;
198 }
199 };
200 if allowed {
201 let previous_sequence = cursor
202 .split_once('-')
203 .and_then(|(number, _)| number.parse::<u64>().ok());
204 let sequence = delivery
205 .split_once('-')
206 .and_then(|(number, _)| number.parse::<u64>().ok());
207 if previous_sequence.and_then(|previous| previous.checked_add(1)) != sequence {
208 payload["resync_required"] = serde_json::Value::Bool(true);
209 }
210 let resync = payload
211 .get("resync_required")
212 .and_then(serde_json::Value::as_bool)
213 == Some(true);
214 let binaries: Vec<Vec<u8>> = payload
215 .get("binaries")
216 .cloned()
217 .and_then(|value| serde_json::from_value(value).ok())
218 .unwrap_or_default();
219 if let Some(object) = payload.as_object_mut() {
220 object.remove("binaries");
221 object.remove("destinations");
222 }
223 let packet = crate::gateway::EmitPacket {
224 namespace: namespace.clone(),
225 event: if resync { "resync" } else { "event" }.to_string(),
226 args: vec![payload],
227 binaries,
228 };
229 if !input.deliver(packet, Duration::from_secs(15)).await {
230 tokio::time::sleep(Duration::from_secs(1)).await;
231 continue;
232 }
233 }
234 if !matches!(
235 store
236 .acknowledge(user, &device, &namespace, &owner, &cursor, &delivery)
237 .await,
238 Ok(true)
239 ) {
240 tokio::time::sleep(Duration::from_secs(1)).await;
241 }
242 }
243 };
244 if !*shutdown.borrow() {
245 tokio::select! {
246 _ = &mut stopped => {}
247 _ = shutdown.changed() => {}
248 _ = input.closed() => {}
249 _ = work => {}
250 }
251 }
252 store.release(user, &device, &namespace, &owner).await;
253 let _ = SocketPayload::default();
254 }
255}
Added lib/events/src/gateway/socket_runtime.rs View lib/events/src/gateway/socket_runtime.rs
@@ -0,0 +1,350 @@
1use std::sync::Arc;
2
3use actor::ActorHost;
4use serde_json::Value;
5use socket::{SocketEvent, SocketHub, SocketPayload};
6use tokio::net::TcpListener;
7use tokio::sync::{mpsc, watch};
8
9use crate::gateway::action::EventProperties;
10use crate::gateway::platform::PlatformClient;
11use crate::gateway::socket_backend::RealtimeBackend;
12use crate::gateway::subscriptions::{SubscriptionRequest, Subscriptions};
13use crate::gateway::{ConsumeResult, EventsPipe, action, is_personal};
14
15pub struct EventActor;
16
17impl actor::VirtualActor for EventActor {
18 type Message = Value;
19 type Response = Value;
20
21 fn receive<'a>(
22 &'a mut self,
23 context: &'a actor::ActorContext,
24 message: Self::Message,
25 ) -> impl std::future::Future<Output = Result<Self::Response, actor::ActorError>> + Send + 'a
26 {
27 async move {
28 context
29 .host()
30 .publish_event(context.id().user_id(), message.clone())
31 .await?;
32 Ok(message)
33 }
34 }
35}
36
37pub fn backend(
38 host: ActorHost,
39 platform: PlatformClient,
40 subscriptions: Subscriptions,
41 push_store: actor::PushStore,
42) -> Arc<RealtimeBackend> {
43 Arc::new(RealtimeBackend::new(
44 host,
45 platform,
46 subscriptions,
47 push_store,
48 ))
49}
50
51async fn handle_socket_event(
52 event: &SocketEvent,
53 host: &ActorHost,
54 platform: &PlatformClient,
55 subscriptions: &Subscriptions,
56) -> Result<Value, String> {
57 match event.event.as_str() {
58 "forget-device" => {
59 if event.data.len() != 1 || !event.binaries.is_empty() {
60 return Err("forget-device requires one JSON argument".to_string());
61 }
62 let device = event.data[0]
63 .get("device_id")
64 .and_then(Value::as_str)
65 .ok_or("device_id is required")?;
66 let device = uuid::Uuid::parse_str(device)
67 .map_err(|_| "device_id must be a UUID")?
68 .to_string();
69 host.push_store()
70 .forget_device(event.user_id, &device)
71 .await?;
72 Ok(Value::Null)
73 }
74 "subscribe" | "unsubscribe" => {
75 if !event.binaries.is_empty() || event.data.len() != 1 {
76 return Err("subscription requires one JSON argument".to_string());
77 }
78 let request: SubscriptionRequest =
79 serde_json::from_value(event.data[0].clone()).map_err(|error| error.to_string())?;
80 if event.event == "unsubscribe" {
81 let id = request.id.as_deref().ok_or("subscription id is required")?;
82 subscriptions
83 .unsubscribe(
84 event.user_id,
85 &event.device_id,
86 &event.namespace,
87 &request.topic,
88 &request.tag,
89 id,
90 )
91 .await?;
92 return Ok(Value::Null);
93 }
94 let id = subscriptions
95 .subscribe(
96 platform,
97 event.user_id,
98 &event.device_id,
99 &event.namespace,
100 request,
101 &event.access_token,
102 )
103 .await?;
104 Ok(serde_json::json!({"id":id}))
105 }
106 _ => {
107 let payload = serde_json::json!({
108 "source": "socket",
109 "event_id": uuid::Uuid::new_v4().to_string(),
110 "namespace": event.namespace,
111 "session_id": event.session_id,
112 "user_id": event.user_id,
113 "event": event.event,
114 "data": event.data,
115 "binaries": event.binaries,
116 });
117 host.actor::<EventActor>(event.user_id)
118 .call(payload)
119 .await
120 .map(|_| Value::Null)
121 .map_err(|error| error.to_string())
122 }
123 }
124}
125
126/// 启动 socket 节点:浏览器上行事件 + actor 唤醒投递。
127pub async fn run_socket_node(
128 hub: SocketHub,
129 mut events: mpsc::Receiver<SocketEvent>,
130 host: ActorHost,
131 platform: PlatformClient,
132 subscriptions: Subscriptions,
133 listen_address: &str,
134 shutdown: watch::Receiver<bool>,
135) -> Result<(), String> {
136 let mut deliveries = host
137 .take_events()
138 .await
139 .ok_or_else(|| "actor events already attached".to_string())?;
140 let listener = TcpListener::bind(listen_address)
141 .await
142 .map_err(|error| format!("binding socket listener: {error}"))?;
143
144 let events_host = host.clone();
145 let events_platform = platform.clone();
146 let events_hub = hub.clone();
147 let events_task = tokio::spawn(async move {
148 let mut lanes = Vec::new();
149 let mut workers = tokio::task::JoinSet::new();
150 for _ in 0..16 {
151 let (sender, mut receiver) = mpsc::channel::<SocketEvent>(64);
152 lanes.push(sender);
153 let host = events_host.clone();
154 let platform = events_platform.clone();
155 let hub = events_hub.clone();
156 let subscriptions = subscriptions.clone();
157 workers.spawn(async move {
158 while let Some(event) = receiver.recv().await {
159 let result =
160 handle_socket_event(&event, &host, &platform, &subscriptions).await;
161 match (event.ack, result) {
162 (Some(ack), Ok(result)) => {
163 let _ = ack.send(SocketPayload {
164 data: vec![serde_json::json!({"ok":true,"data":result})],
165 binaries: Vec::new(),
166 });
167 }
168 (Some(ack), Err(error)) => {
169 let _ = ack.send(SocketPayload {
170 data: vec![serde_json::json!({"ok":false,"error":error})],
171 binaries: Vec::new(),
172 });
173 }
174 (None, Err(error)) => {
175 hub.emit(
176 event.user_id,
177 &event.namespace,
178 "error",
179 vec![serde_json::json!({"message":error})],
180 )
181 .await;
182 }
183 _ => {}
184 }
185 }
186 });
187 }
188 while let Some(event) = events.recv().await {
189 let lane = (event.user_id as u64 % 16) as usize;
190 if let Err(error) = lanes[lane].try_send(event) {
191 let event = error.into_inner();
192 if let Some(ack) = event.ack {
193 let _ = ack.send(SocketPayload {
194 data: vec![
195 serde_json::json!({"ok":false,"error":"user event queue is full"}),
196 ],
197 binaries: Vec::new(),
198 });
199 } else {
200 events_hub
201 .emit(
202 event.user_id,
203 &event.namespace,
204 "error",
205 vec![serde_json::json!({"message":"user event queue is full"})],
206 )
207 .await;
208 }
209 }
210 }
211 drop(lanes);
212 while workers.join_next().await.is_some() {}
213 });
214
215 let delivery_host = host.clone();
216 let delivery_hub = hub.clone();
217 let delivery_task = tokio::spawn(async move {
218 while let Some(event) = deliveries.recv().await {
219 let ready = if event.payload.get("wake").and_then(Value::as_bool) == Some(true) {
220 true
221 } else {
222 let mut payload = event.payload.clone();
223 if payload.get("source").and_then(Value::as_str) == Some("socket")
224 && payload.get("event_id").is_none()
225 {
226 match delivery_host
227 .push_store()
228 .legacy_event_id(event.user_id, delivery_host.node_id(), event.entry_id())
229 .await
230 {
231 Ok(id) => {
232 payload["event_id"] = Value::String(id);
233 }
234 Err(_) => {
235 delivery_host.retry_event(&event).await;
236 continue;
237 }
238 }
239 }
240 delivery_host
241 .push_store()
242 .append(event.user_id, payload)
243 .await
244 .is_ok()
245 };
246 if ready {
247 delivery_hub.wake_user(event.user_id).await;
248 if let Err(error) = delivery_host.complete_event(&event).await {
249 tracing::warn!(%error, "failed to acknowledge push wake");
250 }
251 } else {
252 delivery_host.retry_event(&event).await;
253 }
254 }
255 });
256
257 let result = hub.serve(listener, shutdown).await;
258 hub.shutdown();
259 events_task.abort();
260 delivery_task.abort();
261 let _ = events_task.await;
262 let _ = delivery_task.await;
263 result.map_err(|error| error.to_string())
264}
265
266/// RocketMQ 消费节点:16 lane 分片 + action 扇出。
267pub async fn run_consumer(
268 config: &config::event::consumer::RocketMqConsumerConfig,
269 host: ActorHost,
270 platform: PlatformClient,
271 subscriptions: Subscriptions,
272 mut shutdown: watch::Receiver<bool>,
273) -> Result<(), String> {
274 let mut pipe = EventsPipe::new(config)?;
275 pipe.subscribe_all()
276 .await
277 .map_err(|error| error.to_string())?;
278 pipe.start().await.map_err(|error| error.to_string())?;
279 let (results, mut completed) = mpsc::channel(128);
280 let mut lanes = Vec::new();
281 let mut workers = tokio::task::JoinSet::new();
282 for _ in 0..16 {
283 let (sender, mut receiver) = mpsc::channel(64);
284 lanes.push(sender);
285 let host = host.clone();
286 let platform = platform.clone();
287 let subscriptions = subscriptions.clone();
288 let results = results.clone();
289 workers.spawn(async move {
290 let context = action::EventContext {
291 host: &host,
292 platform: &platform,
293 subscriptions: &subscriptions,
294 };
295 while let Some(message) = receiver.recv().await {
296 let result = action::action(message, &context).await;
297 if results.send(result).await.is_err() {
298 break;
299 }
300 }
301 });
302 }
303 drop(results);
304 if !*shutdown.borrow() {
305 loop {
306 tokio::select! {
307 _ = shutdown.changed() => break,
308 incoming = pipe.next_event() => {
309 let Some(message) = incoming else { break };
310 let lane = event_lane(&message);
311 if let Err(error) = lanes[lane].try_send(message) {
312 pipe.apply_result(ConsumeResult::Retry {message: error.into_inner(),delay_level:0}).await.map_err(|error| error.to_string())?;
313 }
314 }
315 result = completed.recv() => {
316 let Some(result) = result else { break };
317 pipe.apply_result(result).await.map_err(|error| error.to_string())?;
318 }
319 }
320 }
321 }
322 workers.abort_all();
323 while workers.join_next().await.is_some() {}
324 pipe.shutdown().await;
325 Ok(())
326}
327
328fn event_lane(message: &rocketmq_common::common::message::message_ext::MessageExt) -> usize {
329 use rocketmq_common::common::message::MessageTrait;
330 use std::hash::{Hash, Hasher};
331
332 let properties = message.properties();
333 let topic = message.topic();
334 let tag = message
335 .tags_ref()
336 .map(ToString::to_string)
337 .unwrap_or_default();
338 let fields: &[&str] = if is_personal(topic.as_str(), &tag) {
339 &["user"]
340 } else {
341 &["room_id", "issue_id", "pull_id", "repo_id", "org", "user"]
342 };
343 let key = fields.iter().find_map(|field| properties.get(*field));
344 let mut hasher = std::collections::hash_map::DefaultHasher::new();
345 topic.hash(&mut hasher);
346 key.map(|key| key.as_str()).hash(&mut hasher);
347 (hasher.finish() % 16) as usize
348}
349
350pub fn _event_properties_touch(_: &EventProperties) {}
Added lib/events/src/gateway/subscriptions.rs View lib/events/src/gateway/subscriptions.rs
@@ -0,0 +1,419 @@
1use base64::Engine as _;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::gateway::action::EventProperties;
7use crate::gateway::plan;
8use crate::gateway::platform::PlatformClient;
9use actor::{ActorError, RedisConnection, connect_redis_single};
10use config::event::consumer::DEFAULT_TOPIC_PREFIXES;
11use config::event::redis::RedisConfig;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Subscription {
15 pub id: String,
16 pub user_id: i64,
17 pub device_id: String,
18 pub namespace: String,
19 pub topic: String,
20 pub tag: String,
21 pub filter: EventProperties,
22 pub permission_path: Option<String>,
23 pub scope: String,
24}
25
26#[derive(Clone)]
27pub struct Subscriptions {
28 connection: RedisConnection,
29}
30
31impl Subscriptions {
32 pub(crate) fn new(connection: RedisConnection) -> Self {
33 Self { connection }
34 }
35
36 pub async fn connect(config: &RedisConfig) -> Result<Self, ActorError> {
37 Ok(Self::new(connect_redis_single(config).await?))
38 }
39
40 fn validate(topic: &str, tag: &str) -> Result<(), String> {
41 let prefixes = DEFAULT_TOPIC_PREFIXES.map(str::to_string);
42 let plan = plan::plan(&prefixes).map_err(|error| error.to_string())?;
43 if !plan
44 .get(topic)
45 .is_some_and(|tags| tags.split(" || ").any(|known| known == tag))
46 {
47 return Err("unsupported subscription topic or tag".to_string());
48 }
49 Ok(())
50 }
51
52 pub async fn subscribe(
53 &self,
54 platform: &PlatformClient,
55 user_id: i64,
56 device_id: &str,
57 namespace: &str,
58 request: SubscriptionRequest,
59 token: &str,
60 ) -> Result<String, String> {
61 Self::validate(&request.topic, &request.tag)?;
62 if user_id <= 0 || device_id.is_empty() {
63 return Err("authenticated device is required".to_string());
64 }
65 let mut filter = request.filter;
66 filter.normalize()?;
67 let personal = is_personal(&request.topic, &request.tag);
68 if personal {
69 if filter
70 .user_id
71 .as_deref()
72 .is_some_and(|id| id != user_id.to_string())
73 {
74 return Err("personal notifications must target the authenticated user".to_string());
75 }
76 filter.user_id = Some(user_id.to_string());
77 }
78 let _ = token;
79 let path = resource_path(&filter, request.permission_path.as_deref())?;
80 let has_resource = filter.room_id.is_some()
81 || filter.issue_id.is_some()
82 || filter.pull_id.is_some()
83 || filter.repo_id.is_some();
84 if has_resource {
85 if !platform
86 .authorize(user_id, &filter, "SUBSCRIBE")
87 .await
88 .map_err(|error| error.to_string())?
89 {
90 return Err("resource subscription is not authorized".to_string());
91 }
92 } else if filter.user_id.as_deref() != Some(user_id.to_string().as_str()) {
93 return Err(
94 "resource authorization is required for cross-user subscriptions".to_string(),
95 );
96 }
97 if let Some(org) = filter.org_id.as_deref() {
98 if !platform
99 .is_member(user_id, org)
100 .await
101 .map_err(|error| error.to_string())?
102 {
103 return Err("active organization membership is required".to_string());
104 }
105 }
106 let id = request
107 .id
108 .as_deref()
109 .map(Uuid::parse_str)
110 .transpose()
111 .map_err(|_| "invalid subscription id")?
112 .unwrap_or_else(Uuid::new_v4)
113 .to_string();
114 let scope = if personal {
115 format!("user:{user_id}")
116 } else {
117 scopes(&filter)
118 .into_iter()
119 .next()
120 .ok_or("a resource or user filter is required")?
121 };
122 let subscription = Subscription {
123 id: id.clone(),
124 user_id,
125 device_id: device_id.to_string(),
126 namespace: namespace.to_string(),
127 topic: request.topic,
128 tag: request.tag,
129 filter,
130 permission_path: path,
131 scope,
132 };
133 let base = base(&subscription.topic, &subscription.tag);
134 let owner = owner(user_id, device_id, namespace);
135 let field = format!("{owner}:{id}");
136 let mut command = redis::cmd("EVAL");
137 command.arg(r#"
138local old=redis.call('HGET',KEYS[1],ARGV[1])
139if not old and redis.call('SCARD',KEYS[3]) >= 256 then return redis.error_reply('device subscription limit exceeded') end
140if old then redis.call('SREM',ARGV[3]..':scope:'..cjson.decode(old).scope,ARGV[1]) end
141redis.call('HSET',KEYS[1],ARGV[1],ARGV[2])
142redis.call('SADD',KEYS[2],ARGV[1])
143redis.call('SADD',KEYS[3],ARGV[1])
144return 1
145"#).arg(3).arg(format!("{base}:records")).arg(format!("{base}:scope:{}",subscription.scope))
146 .arg(format!("{base}:owner:{owner}")).arg(field)
147 .arg(serde_json::to_string(&subscription).map_err(|error| error.to_string())?).arg(base);
148 let _: i64 = self
149 .connection
150 .query(&command)
151 .await
152 .map_err(|error| error.to_string())?;
153 Ok(id)
154 }
155
156 pub async fn unsubscribe(
157 &self,
158 user_id: i64,
159 device_id: &str,
160 namespace: &str,
161 topic: &str,
162 tag: &str,
163 id: &str,
164 ) -> Result<(), String> {
165 Self::validate(topic, tag)?;
166 let id = Uuid::parse_str(id)
167 .map_err(|_| "invalid subscription id")?
168 .to_string();
169 let base = base(topic, tag);
170 let owner = owner(user_id, device_id, namespace);
171 let mut command = redis::cmd("EVAL");
172 command.arg("local old=redis.call('HGET',KEYS[1],ARGV[1]); if old then redis.call('SREM',ARGV[2]..':scope:'..cjson.decode(old).scope,ARGV[1]); redis.call('HDEL',KEYS[1],ARGV[1]); redis.call('SREM',KEYS[2],ARGV[1]) end; return 1")
173 .arg(2).arg(format!("{base}:records")).arg(format!("{base}:owner:{owner}"))
174 .arg(format!("{owner}:{id}")).arg(base);
175 let _: i64 = self
176 .connection
177 .query(&command)
178 .await
179 .map_err(|error| error.to_string())?;
180 Ok(())
181 }
182
183 pub async fn contains(&self, subscription: &Subscription) -> Result<bool, String> {
184 let mut command = redis::cmd("HGET");
185 command
186 .arg(format!(
187 "{}:records",
188 base(&subscription.topic, &subscription.tag)
189 ))
190 .arg(format!(
191 "{}:{}",
192 owner(
193 subscription.user_id,
194 &subscription.device_id,
195 &subscription.namespace
196 ),
197 subscription.id
198 ));
199 let current: Option<String> = self
200 .connection
201 .query(&command)
202 .await
203 .map_err(|error| error.to_string())?;
204 let expected = serde_json::to_string(subscription).map_err(|error| error.to_string())?;
205 Ok(current.as_deref() == Some(expected.as_str()))
206 }
207
208 pub async fn subscribers(
209 &self,
210 topic: &str,
211 tag: &str,
212 event: &EventProperties,
213 ) -> Result<Vec<Subscription>, String> {
214 let base = base(topic, tag);
215 let mut command = redis::cmd("EVAL");
216 command.arg(
217 r#"
218local seen={}; local result={}
219for i=2,#KEYS do
220 for _,field in ipairs(redis.call('SMEMBERS',KEYS[i])) do
221 if not seen[field] then
222 seen[field]=true
223 local record=redis.call('HGET',KEYS[1],field)
224 if record then table.insert(result,record) end
225 end
226 end
227end
228return result
229"#,
230 );
231 let scopes = scopes(event);
232 command.arg(scopes.len() + 1).arg(format!("{base}:records"));
233 for scope in scopes {
234 command.arg(format!("{base}:scope:{scope}"));
235 }
236 let records: Vec<String> = self
237 .connection
238 .query(&command)
239 .await
240 .map_err(|error| error.to_string())?;
241 records
242 .into_iter()
243 .map(|record| serde_json::from_str(&record).map_err(|error| error.to_string()))
244 .collect()
245 }
246}
247
248#[derive(Debug, Deserialize)]
249pub struct SubscriptionRequest {
250 pub topic: String,
251 #[serde(alias = "label")]
252 pub tag: String,
253 pub id: Option<String>,
254 #[serde(default)]
255 pub filter: EventProperties,
256 pub permission_path: Option<String>,
257}
258
259pub fn is_personal(topic: &str, tag: &str) -> bool {
260 topic == "gitdata-notify"
261 || (topic == "gitdata-chat"
262 && matches!(
263 tag,
264 "mention-create"
265 | "reminder-create"
266 | "membership-create"
267 | "read_state-update"
268 | "presence-update"
269 ))
270}
271
272fn base(topic: &str, tag: &str) -> String {
273 format!("kenevent:subscription:v2:{{{topic}:{tag}}}")
274}
275
276fn owner(user_id: i64, device: &str, namespace: &str) -> String {
277 format!(
278 "{user_id}:{}",
279 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&(device, namespace)).unwrap_or_default())
280 )
281}
282
283fn scopes(properties: &EventProperties) -> Vec<String> {
284 [
285 ("room", &properties.room_id),
286 ("issue", &properties.issue_id),
287 ("pull", &properties.pull_id),
288 ("repo", &properties.repo_id),
289 ("org", &properties.org_id),
290 ("user", &properties.user_id),
291 ]
292 .into_iter()
293 .filter_map(|(kind, id)| id.as_ref().map(|id| format!("{kind}:{id}")))
294 .collect()
295}
296
297/// 权限路径只能是"与 filter 指向同一资源"的绝对路径:防止借路径白名单越权。
298pub fn resource_path(
299 filter: &EventProperties,
300 path: Option<&str>,
301) -> Result<Option<String>, String> {
302 let has_resource = filter.room_id.is_some()
303 || filter.repo_id.is_some()
304 || filter.issue_id.is_some()
305 || filter.pull_id.is_some()
306 || filter.repo_name.is_some();
307 if !has_resource {
308 return if path.is_none() {
309 Ok(None)
310 } else {
311 Err("resource path requires a resource filter".to_string())
312 };
313 }
314 let path = match (path, filter.room_id.as_deref()) {
315 (Some(path), _) => path.to_string(),
316 (None, Some(room)) => format!("/chat/rooms/{room}"),
317 _ => return Err("permission_path is required for this resource".to_string()),
318 };
319 let segments = path
320 .strip_prefix('/')
321 .ok_or("permission_path must be absolute")?
322 .split('/')
323 .collect::<Vec<_>>();
324 if segments.iter().any(|part| {
325 part.is_empty()
326 || *part == "."
327 || *part == ".."
328 || !part
329 .bytes()
330 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
331 }) {
332 return Err("invalid resource permission path".to_string());
333 }
334 let equal_id = |value: &str, expected: &Option<String>| {
335 expected.as_deref().is_some_and(|expected| {
336 Uuid::parse_str(value)
337 .ok()
338 .zip(Uuid::parse_str(expected).ok())
339 .is_some_and(|(a, b)| a == b)
340 })
341 };
342 let valid = match segments.as_slice() {
343 ["chat", "rooms", id] => {
344 equal_id(id, &filter.room_id)
345 && filter.repo_id.is_none()
346 && filter.issue_id.is_none()
347 && filter.pull_id.is_none()
348 }
349 ["repos", _, _, "pulls", id] => {
350 equal_id(id, &filter.pull_id) && filter.room_id.is_none() && filter.issue_id.is_none()
351 }
352 ["issues", _, "boards", board, "issues", id] => {
353 Uuid::parse_str(board).is_ok()
354 && equal_id(id, &filter.issue_id)
355 && filter.room_id.is_none()
356 && filter.pull_id.is_none()
357 }
358 ["repos", space, repo] => {
359 filter
360 .repo_id
361 .as_deref()
362 .is_some_and(|id| Uuid::parse_str(id).is_ok())
363 && filter
364 .repo_name
365 .as_deref()
366 .is_none_or(|name| name == *repo || name == format!("{space}/{repo}"))
367 && filter.room_id.is_none()
368 && filter.issue_id.is_none()
369 && filter.pull_id.is_none()
370 }
371 _ => false,
372 };
373 if !valid {
374 return Err("permission_path does not identify the subscribed resource".to_string());
375 }
376 Ok(Some(path))
377}
378
379impl Subscriptions {
380 /// 列出某设备/命名空间下的全部订阅(RPC ListSubscriptions 用)。
381 pub async fn list(
382 &self,
383 user_id: i64,
384 device_id: &str,
385 namespace: &str,
386 ) -> Result<Vec<Subscription>, String> {
387 let prefixes = DEFAULT_TOPIC_PREFIXES.map(str::to_string);
388 let plan = plan::plan(&prefixes).map_err(|error| error.to_string())?;
389 let owner = owner(user_id, device_id, namespace);
390 let mut out = Vec::new();
391 for (topic, tags) in plan {
392 for tag in tags.split(" || ") {
393 let base = base(&topic, tag);
394 let mut command = redis::cmd("SMEMBERS");
395 command.arg(format!("{base}:owner:{owner}"));
396 let fields: Vec<String> = self
397 .connection
398 .query(&command)
399 .await
400 .map_err(|error| error.to_string())?;
401 for field in fields {
402 let mut command = redis::cmd("HGET");
403 command.arg(format!("{base}:records")).arg(field);
404 let record: Option<String> = self
405 .connection
406 .query(&command)
407 .await
408 .map_err(|error| error.to_string())?;
409 if let Some(record) = record {
410 if let Ok(subscription) = serde_json::from_str::<Subscription>(&record) {
411 out.push(subscription);
412 }
413 }
414 }
415 }
416 }
417 Ok(out)
418 }
419}
Added lib/events/src/rpc/mod.rs View lib/events/src/rpc/mod.rs
@@ -0,0 +1,248 @@
1//! 内部 RPC 的 event 域:EventIngress + Subscription,方法挂在 `EventService` 上。
2
3use internal::Gate;
4use internal::server::InternalRouter;
5use proto::internal::v1::event_ingress_service_server::{
6 EventIngressService as EventIngressTrait, EventIngressServiceServer,
7};
8use proto::internal::v1::subscription_service_server::{
9 SubscriptionService as SubscriptionTrait, SubscriptionServiceServer,
10};
11use proto::internal::v1::{
12 EventFilter, ListSubscriptionsRequest, ListSubscriptionsResponse, PublishRequest,
13 PublishResponse, RevokeRequest, RevokeResponse, SubscribeRequest, SubscribeResponse,
14 SubscriptionView, UnsubscribeRequest, UnsubscribeResponse,
15};
16use tonic::{Request, Response, Status};
17
18use crate::gateway::action::EventProperties;
19use crate::gateway::event_service::EventService;
20use crate::gateway::subscriptions::SubscriptionRequest;
21
22pub fn register_ingress(
23 router: InternalRouter,
24 service: &EventService,
25 gate: Gate,
26) -> InternalRouter {
27 router.add_service(EventIngressServiceServer::with_interceptor(
28 IngressRpc {
29 service: service.clone(),
30 },
31 gate,
32 ))
33}
34
35pub fn register(router: InternalRouter, service: &EventService, gate: Gate) -> InternalRouter {
36 router.add_service(SubscriptionServiceServer::with_interceptor(
37 SubscriptionRpc {
38 service: service.clone(),
39 },
40 gate,
41 ))
42}
43
44#[derive(Clone)]
45struct IngressRpc {
46 service: EventService,
47}
48
49#[derive(Clone)]
50struct SubscriptionRpc {
51 service: EventService,
52}
53
54#[tonic::async_trait]
55impl EventIngressTrait for IngressRpc {
56 async fn publish(
57 &self,
58 request: Request<PublishRequest>,
59 ) -> Result<Response<PublishResponse>, Status> {
60 let meta = internal::interceptor::meta(&request)?;
61 let request = request.into_inner();
62 let Some(envelope) = request.identity.as_ref() else {
63 return Err(Status::unauthenticated("identity envelope is required"));
64 };
65 let identity = self.service.verify_identity(envelope).await?;
66 let body = if request.body.is_empty() {
67 serde_json::Value::Null
68 } else {
69 serde_json::from_slice(&request.body).unwrap_or(serde_json::Value::Null)
70 };
71 let properties = filter_to_properties(request.filter);
72 let delivered = self
73 .service
74 .publish(
75 &identity,
76 &request.topic,
77 &request.tag,
78 properties,
79 body,
80 request.personal,
81 &meta.request_id,
82 )
83 .await?;
84 Ok(Response::new(PublishResponse {
85 delivered,
86 scope: String::new(),
87 }))
88 }
89
90 async fn revoke(
91 &self,
92 request: Request<RevokeRequest>,
93 ) -> Result<Response<RevokeResponse>, Status> {
94 let request = request.into_inner();
95 let Some(envelope) = request.identity.as_ref() else {
96 return Err(Status::unauthenticated("identity envelope is required"));
97 };
98 let identity = self.service.verify_identity(envelope).await?;
99 let closed = self.service.revoke(&identity, &request.reason).await?;
100 Ok(Response::new(RevokeResponse { closed }))
101 }
102}
103
104#[tonic::async_trait]
105impl SubscriptionTrait for SubscriptionRpc {
106 async fn subscribe(
107 &self,
108 request: Request<SubscribeRequest>,
109 ) -> Result<Response<SubscribeResponse>, Status> {
110 let request = request.into_inner();
111 let Some(envelope) = request.identity.as_ref() else {
112 return Err(Status::unauthenticated("identity envelope is required"));
113 };
114 let identity = self.service.verify_identity(envelope).await?;
115 let filter = filter_to_properties(request.filter);
116 let id = self
117 .service
118 .subscribe(
119 identity.user_id,
120 &request.device_id,
121 &request.namespace,
122 SubscriptionRequest {
123 topic: request.topic,
124 tag: request.tag,
125 id: if request.id.is_empty() {
126 None
127 } else {
128 Some(request.id)
129 },
130 filter: filter.clone(),
131 permission_path: if request.permission_path.is_empty() {
132 None
133 } else {
134 Some(request.permission_path)
135 },
136 },
137 "",
138 )
139 .await
140 .map_err(|error| Status::invalid_argument(error))?;
141 Ok(Response::new(SubscribeResponse {
142 id,
143 scope: scope_label(&filter),
144 created_at_unix_ms: 0,
145 }))
146 }
147
148 async fn unsubscribe(
149 &self,
150 request: Request<UnsubscribeRequest>,
151 ) -> Result<Response<UnsubscribeResponse>, Status> {
152 let request = request.into_inner();
153 let Some(envelope) = request.identity.as_ref() else {
154 return Err(Status::unauthenticated("identity envelope is required"));
155 };
156 let identity = self.service.verify_identity(envelope).await?;
157 self.service
158 .unsubscribe(
159 identity.user_id,
160 &request.device_id,
161 &request.namespace,
162 &request.topic,
163 &request.tag,
164 &request.id,
165 )
166 .await
167 .map_err(|error| Status::invalid_argument(error))?;
168 Ok(Response::new(UnsubscribeResponse { removed: true }))
169 }
170
171 async fn list_subscriptions(
172 &self,
173 request: Request<ListSubscriptionsRequest>,
174 ) -> Result<Response<ListSubscriptionsResponse>, Status> {
175 let request = request.into_inner();
176 let Some(envelope) = request.identity.as_ref() else {
177 return Err(Status::unauthenticated("identity envelope is required"));
178 };
179 let identity = self.service.verify_identity(envelope).await?;
180 let subscriptions = self
181 .service
182 .list(identity.user_id, &request.device_id, "/")
183 .await
184 .map_err(|error| Status::unavailable(error))?;
185 let views = subscriptions
186 .into_iter()
187 .map(|subscription| SubscriptionView {
188 id: subscription.id,
189 topic: subscription.topic,
190 tag: subscription.tag,
191 filter: properties_to_filter(&subscription.filter),
192 scope: subscription.scope,
193 device_id: subscription.device_id,
194 namespace: subscription.namespace,
195 })
196 .collect();
197 Ok(Response::new(ListSubscriptionsResponse {
198 subscriptions: views,
199 }))
200 }
201}
202
203fn scope_label(filter: &EventProperties) -> String {
204 [
205 ("room", &filter.room_id),
206 ("issue", &filter.issue_id),
207 ("pull", &filter.pull_id),
208 ("repo", &filter.repo_id),
209 ("org", &filter.org_id),
210 ("user", &filter.user_id),
211 ]
212 .into_iter()
213 .find_map(|(kind, id)| id.as_ref().map(|id| format!("{kind}:{id}")))
214 .unwrap_or_default()
215}
216
217fn filter_to_properties(filter: Option<EventFilter>) -> EventProperties {
218 let filter = filter.unwrap_or_default();
219 EventProperties {
220 user_id: Some(filter.user_id).filter(|value| !value.is_empty()),
221 org_id: Some(filter.org_id).filter(|value| !value.is_empty()),
222 request_id: Some(filter.request_id).filter(|value| !value.is_empty()),
223 repo_id: Some(filter.repo_id).filter(|value| !value.is_empty()),
224 repo_name: Some(filter.repo_name).filter(|value| !value.is_empty()),
225 room_id: Some(filter.room_id).filter(|value| !value.is_empty()),
226 message_id: Some(filter.message_id).filter(|value| !value.is_empty()),
227 thread_id: Some(filter.thread_id).filter(|value| !value.is_empty()),
228 issue_id: Some(filter.issue_id).filter(|value| !value.is_empty()),
229 pull_id: Some(filter.pull_id).filter(|value| !value.is_empty()),
230 timestamp: (filter.timestamp != 0).then_some(filter.timestamp),
231 }
232}
233
234fn properties_to_filter(properties: &EventProperties) -> Option<EventFilter> {
235 Some(EventFilter {
236 user_id: properties.user_id.clone().unwrap_or_default(),
237 org_id: properties.org_id.clone().unwrap_or_default(),
238 request_id: properties.request_id.clone().unwrap_or_default(),
239 repo_id: properties.repo_id.clone().unwrap_or_default(),
240 repo_name: properties.repo_name.clone().unwrap_or_default(),
241 room_id: properties.room_id.clone().unwrap_or_default(),
242 message_id: properties.message_id.clone().unwrap_or_default(),
243 thread_id: properties.thread_id.clone().unwrap_or_default(),
244 issue_id: properties.issue_id.clone().unwrap_or_default(),
245 pull_id: properties.pull_id.clone().unwrap_or_default(),
246 timestamp: properties.timestamp.unwrap_or_default(),
247 })
248}
Modified lib/events/src/lib.rs View lib/events/src/lib.rs
@@ -8,3 +8,8 @@
88pub use handler::EventHandler;
99pub use outbox::emit_retry;
1010pub use queue::{Envelope, Labels};
11
12#[cfg(feature = "gateway")]
13pub mod gateway;
14#[cfg(feature = "gateway")]
15pub mod rpc;
Modified lib/events/Cargo.toml View lib/events/Cargo.toml
@@ -16,7 +16,35 @@
1616serde = { workspace = true, features = ["derive"] }
1717serde_json = { workspace = true }
1818thiserror = { workspace = true }
19tokio = { workspace = true, features = ["time"] }
19tokio = { workspace = true, features = ["time", "rt", "net", "sync", "macros"] }
20tonic = { workspace = true, optional = true }
2021tokio-util = { workspace = true }
2122tracing = { workspace = true }
2223uuid = { workspace = true }
24
25actor = { workspace = true, optional = true }
26async-trait = { workspace = true, optional = true }
27base64 = { workspace = true, optional = true }
28internal = { workspace = true, optional = true }
29proto = { workspace = true, optional = true }
30redis = { workspace = true, optional = true }
31reqwest = { workspace = true, optional = true }
32rocketmq-client-rust = { workspace = true, optional = true }
33rocketmq-common = { workspace = true, optional = true }
34socket = { workspace = true, optional = true }
35
36[features]
37default = []
38gateway = [
39 "dep:actor",
40 "dep:async-trait",
41 "dep:base64",
42 "dep:internal",
43 "dep:proto",
44 "dep:redis",
45 "dep:reqwest",
46 "dep:rocketmq-client-rust",
47 "dep:rocketmq-common",
48 "dep:socket",
49 "dep:tonic",
50]