core-framework 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. core_framework/__init__.py +0 -0
  2. core_framework/alembic/comment/alembic/README +1 -0
  3. core_framework/alembic/comment/alembic/env.py +72 -0
  4. core_framework/alembic/comment/alembic/script.py.mako +28 -0
  5. core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
  6. core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
  7. core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
  8. core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
  9. core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
  10. core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
  11. core_framework/alembic/comment/alembic.ini +52 -0
  12. core_framework/alembic/extension/alembic/README +1 -0
  13. core_framework/alembic/extension/alembic/env.py +98 -0
  14. core_framework/alembic/extension/alembic/script.py.mako +28 -0
  15. core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
  16. core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
  17. core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
  18. core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
  19. core_framework/alembic/extension/alembic.ini +147 -0
  20. core_framework/alembic/moderation/alembic/README +1 -0
  21. core_framework/alembic/moderation/alembic/env.py +98 -0
  22. core_framework/alembic/moderation/alembic/script.py.mako +28 -0
  23. core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
  24. core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
  25. core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
  26. core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
  27. core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
  28. core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
  29. core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
  30. core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
  31. core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
  32. core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
  33. core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
  34. core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
  35. core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
  36. core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
  37. core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
  38. core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
  39. core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
  40. core_framework/alembic/moderation/alembic.ini +147 -0
  41. core_framework/alembic/post/alembic/README +1 -0
  42. core_framework/alembic/post/alembic/env.py +97 -0
  43. core_framework/alembic/post/alembic/script.py.mako +28 -0
  44. core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
  45. core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
  46. core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
  47. core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
  48. core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
  49. core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
  50. core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
  51. core_framework/alembic/post/alembic.ini +149 -0
  52. core_framework/alembic/user/alembic/README +1 -0
  53. core_framework/alembic/user/alembic/env.py +98 -0
  54. core_framework/alembic/user/alembic/script.py.mako +28 -0
  55. core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
  56. core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
  57. core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
  58. core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
  59. core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
  60. core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
  61. core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
  62. core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
  63. core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
  64. core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
  65. core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
  66. core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
  67. core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
  68. core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
  69. core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
  70. core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
  71. core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
  72. core_framework/alembic/user/alembic.ini +147 -0
  73. core_framework/api/__init__.py +0 -0
  74. core_framework/api/admin/__init__.py +0 -0
  75. core_framework/api/admin/comments/router.py +69 -0
  76. core_framework/api/admin/comments/schemas.py +53 -0
  77. core_framework/api/admin/moderation/__init__.py +0 -0
  78. core_framework/api/admin/moderation/router.py +205 -0
  79. core_framework/api/admin/moderation/schemas.py +110 -0
  80. core_framework/api/admin/posts/router.py +62 -0
  81. core_framework/api/admin/posts/schemas.py +29 -0
  82. core_framework/api/admin/router.py +17 -0
  83. core_framework/api/admin/users/__init__.py +0 -0
  84. core_framework/api/admin/users/router.py +181 -0
  85. core_framework/api/admin/users/schemas.py +137 -0
  86. core_framework/api/auth/__init__.py +0 -0
  87. core_framework/api/auth/router.py +21 -0
  88. core_framework/api/auth/schemas.py +28 -0
  89. core_framework/api/comments/authenticated/router.py +126 -0
  90. core_framework/api/comments/authenticated/schemas.py +27 -0
  91. core_framework/api/comments/public/router.py +103 -0
  92. core_framework/api/comments/public/schemas.py +36 -0
  93. core_framework/api/comments/router.py +9 -0
  94. core_framework/api/comments/schemas.py +17 -0
  95. core_framework/api/dependencies.py +168 -0
  96. core_framework/api/events/router.py +39 -0
  97. core_framework/api/events/schemas.py +20 -0
  98. core_framework/api/posts/authenticated/router.py +83 -0
  99. core_framework/api/posts/authenticated/schemas.py +37 -0
  100. core_framework/api/posts/public/router.py +100 -0
  101. core_framework/api/posts/public/schemas.py +39 -0
  102. core_framework/api/posts/router.py +9 -0
  103. core_framework/api/posts/schemas.py +39 -0
  104. core_framework/api/router.py +19 -0
  105. core_framework/api/schemas.py +9 -0
  106. core_framework/api/system/__init__.py +0 -0
  107. core_framework/api/system/router.py +108 -0
  108. core_framework/api/users/__init__.py +0 -0
  109. core_framework/api/users/authenticated/__init__.py +0 -0
  110. core_framework/api/users/authenticated/router.py +244 -0
  111. core_framework/api/users/authenticated/schemas.py +81 -0
  112. core_framework/api/users/public/__init__.py +0 -0
  113. core_framework/api/users/public/router.py +25 -0
  114. core_framework/api/users/public/schemas.py +7 -0
  115. core_framework/api/users/router.py +9 -0
  116. core_framework/api/users/shared/schemas.py +174 -0
  117. core_framework/application/__init__.py +0 -0
  118. core_framework/application/auth/__init__.py +0 -0
  119. core_framework/application/auth/access_service.py +26 -0
  120. core_framework/application/auth/auth_service.py +10 -0
  121. core_framework/application/auth/models.py +10 -0
  122. core_framework/application/bootstrap.py +19 -0
  123. core_framework/application/comments/admin_service.py +236 -0
  124. core_framework/application/comments/aggregation_service.py +28 -0
  125. core_framework/application/comments/authenticated_service.py +89 -0
  126. core_framework/application/comments/public_service.py +218 -0
  127. core_framework/application/events/README.md +26 -0
  128. core_framework/application/events/event_service.py +51 -0
  129. core_framework/application/events/event_token.py +46 -0
  130. core_framework/application/events/models.py +9 -0
  131. core_framework/application/moderation/__init__.py +0 -0
  132. core_framework/application/moderation/appeal_service.py +98 -0
  133. core_framework/application/moderation/moderator_service.py +46 -0
  134. core_framework/application/moderation/report_service.py +180 -0
  135. core_framework/application/moderation/scheduled_service.py +5 -0
  136. core_framework/application/moderation/user_service.py +180 -0
  137. core_framework/application/posts/admin_service.py +104 -0
  138. core_framework/application/posts/aggregation_service.py +28 -0
  139. core_framework/application/posts/authenticated_service.py +72 -0
  140. core_framework/application/posts/public_service.py +197 -0
  141. core_framework/application/shared/__init__.py +0 -0
  142. core_framework/application/shared/enums.py +16 -0
  143. core_framework/application/shared/exceptions.py +16 -0
  144. core_framework/application/shared/user_agent.py +24 -0
  145. core_framework/application/users/__init__.py +0 -0
  146. core_framework/application/users/admin_service.py +298 -0
  147. core_framework/application/users/authenticated_service.py +179 -0
  148. core_framework/application/users/public_service.py +7 -0
  149. core_framework/application/users/scheduled_service.py +5 -0
  150. core_framework/bundled_alembic.py +57 -0
  151. core_framework/core/__init__.py +37 -0
  152. core_framework/core/cache.py +234 -0
  153. core_framework/core/context.py +14 -0
  154. core_framework/core/database.py +111 -0
  155. core_framework/core/exception_handlers/__init__.py +3 -0
  156. core_framework/core/exception_handlers/comment.py +99 -0
  157. core_framework/core/exception_handlers/common.py +5 -0
  158. core_framework/core/exception_handlers/moderation.py +104 -0
  159. core_framework/core/exception_handlers/post.py +54 -0
  160. core_framework/core/exception_handlers/setup.py +80 -0
  161. core_framework/core/exception_handlers/user.py +72 -0
  162. core_framework/core/http_client.py +64 -0
  163. core_framework/core/logging.py +99 -0
  164. core_framework/core/middleware.py +64 -0
  165. core_framework/core/observability.py +36 -0
  166. core_framework/core/pagination.py +203 -0
  167. core_framework/core/redis.py +135 -0
  168. core_framework/core/runtime.py +66 -0
  169. core_framework/core/settings.py +189 -0
  170. core_framework/domains/__init__.py +0 -0
  171. core_framework/domains/comment/README.md +243 -0
  172. core_framework/domains/comment/__init__.py +25 -0
  173. core_framework/domains/comment/constants.py +3 -0
  174. core_framework/domains/comment/dependencies.py +29 -0
  175. core_framework/domains/comment/enums.py +11 -0
  176. core_framework/domains/comment/exceptions.py +31 -0
  177. core_framework/domains/comment/models.py +54 -0
  178. core_framework/domains/comment/repository.py +947 -0
  179. core_framework/domains/comment/service.py +259 -0
  180. core_framework/domains/moderation/README.md +138 -0
  181. core_framework/domains/moderation/__init__.py +47 -0
  182. core_framework/domains/moderation/dependencies.py +29 -0
  183. core_framework/domains/moderation/enums.py +62 -0
  184. core_framework/domains/moderation/exceptions.py +31 -0
  185. core_framework/domains/moderation/models.py +94 -0
  186. core_framework/domains/moderation/repository.py +828 -0
  187. core_framework/domains/moderation/service.py +334 -0
  188. core_framework/domains/post/README.md +182 -0
  189. core_framework/domains/post/__init__.py +22 -0
  190. core_framework/domains/post/constants.py +3 -0
  191. core_framework/domains/post/dependencies.py +29 -0
  192. core_framework/domains/post/enums.py +18 -0
  193. core_framework/domains/post/exceptions.py +21 -0
  194. core_framework/domains/post/models.py +53 -0
  195. core_framework/domains/post/repository.py +791 -0
  196. core_framework/domains/post/service.py +204 -0
  197. core_framework/domains/user/README.md +74 -0
  198. core_framework/domains/user/__init__.py +39 -0
  199. core_framework/domains/user/constants.py +8 -0
  200. core_framework/domains/user/dependencies.py +29 -0
  201. core_framework/domains/user/enums.py +19 -0
  202. core_framework/domains/user/exceptions.py +31 -0
  203. core_framework/domains/user/models.py +124 -0
  204. core_framework/domains/user/repository.py +612 -0
  205. core_framework/domains/user/service.py +257 -0
  206. core_framework/domains/user/utils.py +182 -0
  207. core_framework/main.py +104 -0
  208. core_framework/worker/__init__.py +0 -0
  209. core_framework/worker/main.py +56 -0
  210. core_framework/worker/schedules/__init__.py +35 -0
  211. core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
  212. core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
  213. core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
  214. core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
  215. core_framework/worker/tasks/__init__.py +11 -0
  216. core_framework/worker/tasks/process_account_deletion.py +13 -0
  217. core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
  218. core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
  219. core_framework/worker/tasks/process_mute_lift.py +13 -0
  220. core_framework-0.3.0.dist-info/METADATA +22 -0
  221. core_framework-0.3.0.dist-info/RECORD +222 -0
  222. core_framework-0.3.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+ from core_framework.application.shared.enums import SubjectType
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, kw_only=True)
7
+ class Event:
8
+ subject_type: SubjectType
9
+ subject: str
File without changes
@@ -0,0 +1,98 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from loguru import logger
5
+
6
+ import core_framework.domains.moderation.dependencies as moderation_deps
7
+ import core_framework.domains.user.dependencies as user_deps
8
+ from core_framework.application.moderation.user_service import clear_user_restriction
9
+ from core_framework.domains.moderation import AppealDecision, ModerationActionType
10
+
11
+
12
+ async def decide_appeal(
13
+ *,
14
+ actor_id: str,
15
+ appeal_id: int,
16
+ decision: AppealDecision,
17
+ reason: str,
18
+ ) -> dict[str, Any]:
19
+ raw_appeal = await moderation_deps.moderation_service.decide_appeal(
20
+ actor_id=actor_id,
21
+ appeal_id=appeal_id,
22
+ decision=decision,
23
+ reason=reason,
24
+ )
25
+ await moderation_deps.moderation_service.record_moderation_action(
26
+ actor_id=actor_id,
27
+ action_type=ModerationActionType.DECIDE_APPEAL,
28
+ target_user_id=raw_appeal.user_id,
29
+ action_metadata={
30
+ "appeal_id": raw_appeal.id,
31
+ "decision": decision,
32
+ "reason": reason,
33
+ },
34
+ )
35
+ user_ids = {raw_appeal.user_id} if raw_appeal.reviewer_id is None else {raw_appeal.user_id, raw_appeal.reviewer_id}
36
+ user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(user_ids=user_ids)
37
+
38
+ if raw_appeal.status == AppealDecision.APPROVED:
39
+ try:
40
+ await clear_user_restriction(actor_id=actor_id, user_id=raw_appeal.user_id)
41
+ except Exception as e:
42
+ logger.error(f"Failed to clear restriction after appeal approval {appeal_id=} {raw_appeal.user_id=}: {e}")
43
+
44
+ return {
45
+ "id": raw_appeal.id,
46
+ "user": {
47
+ "user_id": raw_appeal.user_id,
48
+ "username": user_identity_mapping[raw_appeal.user_id].username,
49
+ "display_name": user_identity_mapping[raw_appeal.user_id].display_name,
50
+ },
51
+ "justification": raw_appeal.justification,
52
+ "reviewer": {
53
+ "user_id": raw_appeal.reviewer_id,
54
+ "username": user_identity_mapping[raw_appeal.reviewer_id].username,
55
+ "display_name": user_identity_mapping[raw_appeal.reviewer_id].display_name,
56
+ }
57
+ if raw_appeal.reviewer_id is not None
58
+ else None,
59
+ "decision_reason": raw_appeal.decision_reason,
60
+ "status": raw_appeal.status,
61
+ "created_at": raw_appeal.created_at,
62
+ "updated_at": raw_appeal.updated_at,
63
+ }
64
+
65
+
66
+ async def retrieve_appeals(*, status: AppealDecision | None, cursor: datetime, limit: int) -> list[dict[str, Any]]:
67
+ raw_appeals = await moderation_deps.moderation_service.retrieve_appeals(
68
+ status=status,
69
+ cursor=cursor,
70
+ limit=limit,
71
+ )
72
+ user_ids = {appeal.user_id for appeal in raw_appeals} | {
73
+ appeal.reviewer_id for appeal in raw_appeals if appeal.reviewer_id is not None
74
+ }
75
+ user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(user_ids=user_ids)
76
+ return [
77
+ {
78
+ "id": appeal.id,
79
+ "user": {
80
+ "user_id": appeal.user_id,
81
+ "username": user_identity_mapping[appeal.user_id].username,
82
+ "display_name": user_identity_mapping[appeal.user_id].display_name,
83
+ },
84
+ "justification": appeal.justification,
85
+ "reviewer": {
86
+ "user_id": appeal.reviewer_id,
87
+ "username": user_identity_mapping[appeal.reviewer_id].username,
88
+ "display_name": user_identity_mapping[appeal.reviewer_id].display_name,
89
+ }
90
+ if appeal.reviewer_id is not None
91
+ else None,
92
+ "decision_reason": appeal.decision_reason,
93
+ "status": appeal.status,
94
+ "created_at": appeal.created_at,
95
+ "updated_at": appeal.updated_at,
96
+ }
97
+ for appeal in raw_appeals
98
+ ]
@@ -0,0 +1,46 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ import core_framework.domains.moderation.dependencies as moderation_deps
5
+ import core_framework.domains.user.dependencies as user_deps
6
+
7
+
8
+ async def retrieve_moderation_actions_strong(
9
+ *,
10
+ actor_id: str,
11
+ cursor: datetime,
12
+ limit: int,
13
+ ) -> list[dict[str, Any]]:
14
+ actions = await moderation_deps.moderation_service.retrieve_moderation_actions_strong(
15
+ actor_id=actor_id,
16
+ cursor=cursor,
17
+ limit=limit,
18
+ )
19
+ actor_ids = {action.actor_id for action in actions}
20
+ target_user_ids = {action.target_user_id for action in actions if action.target_user_id is not None}
21
+ user_ids = actor_ids | target_user_ids
22
+ user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(user_ids=user_ids)
23
+
24
+ return [
25
+ {
26
+ "id": action.id,
27
+ "actor": {
28
+ "user_id": action.actor_id,
29
+ "username": user_identity_mapping[action.actor_id].username,
30
+ "display_name": user_identity_mapping[action.actor_id].display_name,
31
+ },
32
+ "target_user": (
33
+ {
34
+ "user_id": action.target_user_id,
35
+ "username": user_identity_mapping[action.target_user_id].username,
36
+ "display_name": user_identity_mapping[action.target_user_id].display_name,
37
+ }
38
+ if action.target_user_id is not None
39
+ else None
40
+ ),
41
+ "action_type": action.action_type,
42
+ "action_metadata": action.action_metadata,
43
+ "created_at": action.created_at,
44
+ }
45
+ for action in actions
46
+ ]
@@ -0,0 +1,180 @@
1
+ from asyncio import TaskGroup
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ import core_framework.domains.comment.dependencies as comment_deps
6
+ import core_framework.domains.moderation.dependencies as moderation_deps
7
+ import core_framework.domains.post.dependencies as post_deps
8
+ import core_framework.domains.user.dependencies as user_deps
9
+ from core_framework.domains.moderation import Report, ReportType
10
+
11
+
12
+ def _build_report_item(
13
+ report: Report,
14
+ report_type: ReportType,
15
+ reporter: dict[str, Any],
16
+ preview: list[dict[str, str]],
17
+ ) -> dict[str, Any]:
18
+ return {
19
+ "type": report_type.value,
20
+ "id": report.id,
21
+ "reporter": reporter,
22
+ "target_id": report.target_id,
23
+ "category": report.category,
24
+ "reason": report.reason,
25
+ "created_at": report.created_at,
26
+ "preview": preview,
27
+ }
28
+
29
+
30
+ async def _retrieve_user_reports(
31
+ *,
32
+ reporter_id: str | None,
33
+ target_id: str | None,
34
+ cursor: datetime,
35
+ limit: int,
36
+ ) -> list[dict[str, Any]]:
37
+ raw_reports = await moderation_deps.moderation_service.retrieve_user_reports(
38
+ reporter_id=reporter_id,
39
+ target_id=target_id,
40
+ cursor=cursor,
41
+ limit=limit,
42
+ )
43
+ if not raw_reports:
44
+ return []
45
+
46
+ reporter_ids = {r.reporter_id for r in raw_reports}
47
+ target_ids = {r.target_id for r in raw_reports}
48
+ user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(
49
+ user_ids=reporter_ids | target_ids
50
+ )
51
+
52
+ result: list[dict[str, Any]] = []
53
+ for report in raw_reports:
54
+ reporter_identity = user_identity_mapping[report.reporter_id]
55
+ reporter = {
56
+ "user_id": report.reporter_id,
57
+ "username": reporter_identity.username,
58
+ "display_name": reporter_identity.display_name,
59
+ }
60
+ target_identity = user_identity_mapping[report.target_id]
61
+ preview = [
62
+ {"key": "username", "value": target_identity.username},
63
+ {"key": "display_name", "value": target_identity.display_name or ""},
64
+ {"key": "role", "value": target_identity.role.value},
65
+ ]
66
+ result.append(_build_report_item(report, ReportType.USER, reporter, preview))
67
+
68
+ return result
69
+
70
+
71
+ async def _retrieve_post_reports(
72
+ *,
73
+ reporter_id: str | None,
74
+ target_id: str | None,
75
+ cursor: datetime,
76
+ limit: int,
77
+ ) -> list[dict[str, Any]]:
78
+ raw_reports = await moderation_deps.moderation_service.retrieve_post_reports(
79
+ reporter_id=reporter_id,
80
+ target_id=target_id,
81
+ cursor=cursor,
82
+ limit=limit,
83
+ )
84
+ if not raw_reports:
85
+ return []
86
+
87
+ reporter_ids = {r.reporter_id for r in raw_reports}
88
+ target_ids = {r.target_id for r in raw_reports}
89
+ async with TaskGroup() as tg:
90
+ identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=reporter_ids))
91
+ content_task = tg.create_task(post_deps.post_service.retrieve_post_content_mapping(post_ids=target_ids))
92
+ user_identity_mapping = identity_task.result()
93
+ post_content_mapping = content_task.result()
94
+
95
+ result: list[dict[str, Any]] = []
96
+ for report in raw_reports:
97
+ reporter_identity = user_identity_mapping[report.reporter_id]
98
+ reporter = {
99
+ "user_id": report.reporter_id,
100
+ "username": reporter_identity.username,
101
+ "display_name": reporter_identity.display_name,
102
+ }
103
+ content = post_content_mapping[report.target_id]
104
+ preview = [{"key": "post_content", "value": content}]
105
+ result.append(_build_report_item(report, ReportType.POST, reporter, preview))
106
+
107
+ return result
108
+
109
+
110
+ async def _retrieve_comment_reports(
111
+ *,
112
+ reporter_id: str | None,
113
+ target_id: str | None,
114
+ cursor: datetime,
115
+ limit: int,
116
+ ) -> list[dict[str, Any]]:
117
+ raw_reports = await moderation_deps.moderation_service.retrieve_comment_reports(
118
+ reporter_id=reporter_id,
119
+ target_id=target_id,
120
+ cursor=cursor,
121
+ limit=limit,
122
+ )
123
+ if not raw_reports:
124
+ return []
125
+
126
+ reporter_ids = {r.reporter_id for r in raw_reports}
127
+ target_ids = {r.target_id for r in raw_reports}
128
+ async with TaskGroup() as tg:
129
+ identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=reporter_ids))
130
+ content_task = tg.create_task(
131
+ comment_deps.comment_service.retrieve_comment_content_mapping(comment_ids=target_ids)
132
+ )
133
+ user_identity_mapping = identity_task.result()
134
+ comment_content_mapping = content_task.result()
135
+
136
+ result: list[dict[str, Any]] = []
137
+ for report in raw_reports:
138
+ reporter_identity = user_identity_mapping[report.reporter_id]
139
+ reporter = {
140
+ "user_id": report.reporter_id,
141
+ "username": reporter_identity.username,
142
+ "display_name": reporter_identity.display_name,
143
+ }
144
+ content = comment_content_mapping[report.target_id]
145
+ preview = [{"key": "comment_content", "value": content}]
146
+ result.append(_build_report_item(report, ReportType.COMMENT, reporter, preview))
147
+
148
+ return result
149
+
150
+
151
+ async def retrieve_reports(
152
+ *,
153
+ report_type: ReportType,
154
+ reporter_id: str | None,
155
+ target_id: str | None,
156
+ cursor: datetime,
157
+ limit: int,
158
+ ) -> list[dict[str, Any]]:
159
+ match report_type:
160
+ case ReportType.USER:
161
+ return await _retrieve_user_reports(
162
+ reporter_id=reporter_id,
163
+ target_id=target_id,
164
+ cursor=cursor,
165
+ limit=limit,
166
+ )
167
+ case ReportType.POST:
168
+ return await _retrieve_post_reports(
169
+ reporter_id=reporter_id,
170
+ target_id=target_id,
171
+ cursor=cursor,
172
+ limit=limit,
173
+ )
174
+ case ReportType.COMMENT:
175
+ return await _retrieve_comment_reports(
176
+ reporter_id=reporter_id,
177
+ target_id=target_id,
178
+ cursor=cursor,
179
+ limit=limit,
180
+ )
@@ -0,0 +1,5 @@
1
+ import core_framework.domains.moderation.dependencies as moderation_deps
2
+
3
+
4
+ async def retrieve_expired_mute_user_ids() -> list[str]:
5
+ return await moderation_deps.moderation_service.retrieve_expired_mute_user_ids()
@@ -0,0 +1,180 @@
1
+ from asyncio import TaskGroup
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ import core_framework.domains.comment.dependencies as comment_deps
6
+ import core_framework.domains.moderation.dependencies as moderation_deps
7
+ import core_framework.domains.post.dependencies as post_deps
8
+ import core_framework.domains.user.dependencies as user_deps
9
+ from core_framework.application.shared.enums import RedisKeys
10
+ from core_framework.application.shared.exceptions import ForbiddenException, UserNotFoundException
11
+ from core_framework.core.cache import invalidate_cache
12
+ from core_framework.domains.moderation import ModerationActionType, RestrictionCategory, RestrictionType
13
+ from core_framework.domains.user import UserIdentity, UserRole
14
+
15
+
16
+ async def ban_user(
17
+ *,
18
+ actor_id: str,
19
+ user_id: str,
20
+ category: RestrictionCategory,
21
+ reason: str,
22
+ ) -> None:
23
+ identity = await user_deps.user_service.retrieve_user_identity_strong(user_id=user_id)
24
+ if identity == UserIdentity.DEFAULT:
25
+ raise UserNotFoundException()
26
+ if identity.role == UserRole.ADMIN:
27
+ raise ForbiddenException("Admin users cannot be banned")
28
+ restriction = await moderation_deps.moderation_service.retrieve_user_restriction_strong(user_id=user_id)
29
+ if restriction.status == RestrictionType.BANNED:
30
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=restriction.status)
31
+ return
32
+ await moderation_deps.moderation_service.ban_user(
33
+ actor_id=actor_id,
34
+ user_id=user_id,
35
+ reason=reason,
36
+ category=category,
37
+ )
38
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=RestrictionType.BANNED)
39
+ await moderation_deps.moderation_service.record_moderation_action(
40
+ actor_id=actor_id,
41
+ action_type=ModerationActionType.BAN,
42
+ target_user_id=user_id,
43
+ action_metadata={"reason": reason, "category": category},
44
+ )
45
+ await invalidate_cache(RedisKeys.USER_DETAIL, user_id=user_id)
46
+
47
+
48
+ async def mute_user(*, actor_id: str, user_id: str, category: RestrictionCategory, reason: str) -> None:
49
+ identity = await user_deps.user_service.retrieve_user_identity_strong(user_id=user_id)
50
+ if identity == UserIdentity.DEFAULT:
51
+ raise UserNotFoundException()
52
+ restriction = await moderation_deps.moderation_service.retrieve_user_restriction_strong(user_id=user_id)
53
+ if restriction.status in (RestrictionType.MUTED, RestrictionType.BANNED):
54
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=restriction.status)
55
+ return
56
+ await moderation_deps.moderation_service.mute_user(
57
+ actor_id=actor_id,
58
+ user_id=user_id,
59
+ reason=reason,
60
+ category=category,
61
+ )
62
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=RestrictionType.MUTED)
63
+ await moderation_deps.moderation_service.record_moderation_action(
64
+ actor_id=actor_id,
65
+ action_type=ModerationActionType.MUTE,
66
+ target_user_id=user_id,
67
+ action_metadata={"reason": reason, "category": category},
68
+ )
69
+ await invalidate_cache(RedisKeys.USER_DETAIL, user_id=user_id)
70
+
71
+
72
+ async def warn_user(*, actor_id: str, user_id: str, category: RestrictionCategory, reason: str) -> None:
73
+ identity = await user_deps.user_service.retrieve_user_identity_strong(user_id=user_id)
74
+ if identity == UserIdentity.DEFAULT:
75
+ raise UserNotFoundException()
76
+ restriction = await moderation_deps.moderation_service.retrieve_user_restriction_strong(user_id=user_id)
77
+ if restriction.status in (RestrictionType.WARNED, RestrictionType.MUTED, RestrictionType.BANNED):
78
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=restriction.status)
79
+ return
80
+ await moderation_deps.moderation_service.warn_user(
81
+ actor_id=actor_id,
82
+ user_id=user_id,
83
+ reason=reason,
84
+ category=category,
85
+ )
86
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=RestrictionType.WARNED)
87
+ await moderation_deps.moderation_service.record_moderation_action(
88
+ actor_id=actor_id,
89
+ action_type=ModerationActionType.WARN,
90
+ target_user_id=user_id,
91
+ action_metadata={"reason": reason, "category": category},
92
+ )
93
+ await invalidate_cache(RedisKeys.USER_DETAIL, user_id=user_id)
94
+
95
+
96
+ async def clear_user_restriction(*, actor_id: str, user_id: str) -> None:
97
+ identity = await user_deps.user_service.retrieve_user_identity_strong(user_id=user_id)
98
+ if identity == UserIdentity.DEFAULT:
99
+ raise UserNotFoundException()
100
+ restriction = await moderation_deps.moderation_service.retrieve_user_restriction_strong(user_id=user_id)
101
+ if restriction.status == RestrictionType.ACTIVE:
102
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=restriction.status)
103
+ return
104
+ await moderation_deps.moderation_service.clear_user_restriction(actor_id=actor_id, user_id=user_id)
105
+ await _sync_user_restriction_lookups(user_id=user_id, restriction_status=RestrictionType.ACTIVE)
106
+ await moderation_deps.moderation_service.record_moderation_action(
107
+ actor_id=actor_id,
108
+ action_type=ModerationActionType.UNRESTRICT,
109
+ target_user_id=user_id,
110
+ )
111
+ await invalidate_cache(RedisKeys.USER_DETAIL, user_id=user_id)
112
+
113
+
114
+ async def remove_user_report(*, report_id: int) -> None:
115
+ await moderation_deps.moderation_service.remove_user_report(report_id=report_id)
116
+
117
+
118
+ async def remove_post_report(*, report_id: int) -> None:
119
+ await moderation_deps.moderation_service.remove_post_report(report_id=report_id)
120
+
121
+
122
+ async def remove_comment_report(*, report_id: int) -> None:
123
+ await moderation_deps.moderation_service.remove_comment_report(report_id=report_id)
124
+
125
+
126
+ async def retrieve_restriction_history(
127
+ *,
128
+ user_id: str,
129
+ cursor: datetime,
130
+ limit: int,
131
+ ) -> list[dict[str, Any]]:
132
+ restriction_history = await moderation_deps.moderation_service.retrieve_restriction_history(
133
+ user_id=user_id,
134
+ cursor=cursor,
135
+ limit=limit,
136
+ )
137
+ actor_ids = {entry.actor_id for entry in restriction_history if entry.actor_id is not None}
138
+ user_identity_mapping = await user_deps.user_service.retrieve_user_identity_mapping(user_ids=actor_ids)
139
+ return [
140
+ {
141
+ "user_id": entry.user_id,
142
+ "action": entry.action,
143
+ "restriction_type": entry.restriction_type,
144
+ "category": entry.category,
145
+ "reason": entry.reason,
146
+ "actor": {
147
+ "user_id": entry.actor_id,
148
+ "username": user_identity_mapping[entry.actor_id].username,
149
+ "display_name": user_identity_mapping[entry.actor_id].display_name,
150
+ }
151
+ if entry.actor_id is not None
152
+ else None,
153
+ "created_at": entry.created_at,
154
+ }
155
+ for entry in restriction_history
156
+ ]
157
+
158
+
159
+ async def _sync_user_restriction_lookups(*, user_id: str, restriction_status: RestrictionType) -> None:
160
+ # Keep post- and comment-domain lookups aligned with moderation truth, including no-op
161
+ # moderation calls (early returns) so retries can heal cross-domain drift from prior partial failures.
162
+ if restriction_status in (RestrictionType.MUTED, RestrictionType.BANNED):
163
+ restriction_type = restriction_status.value
164
+ async with TaskGroup() as tg:
165
+ tg.create_task(
166
+ post_deps.post_service.add_user_restriction_lookup(
167
+ user_id=user_id,
168
+ restriction_type=restriction_type,
169
+ )
170
+ )
171
+ tg.create_task(
172
+ comment_deps.comment_service.add_user_restriction_lookup(
173
+ user_id=user_id,
174
+ restriction_type=restriction_type,
175
+ )
176
+ )
177
+ return
178
+ async with TaskGroup() as tg:
179
+ tg.create_task(post_deps.post_service.remove_user_restriction_lookup(user_id=user_id))
180
+ tg.create_task(comment_deps.comment_service.remove_user_restriction_lookup(user_id=user_id))
@@ -0,0 +1,104 @@
1
+ from asyncio import TaskGroup
2
+ from collections import defaultdict
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ import core_framework.domains.comment.dependencies as comment_deps
7
+ import core_framework.domains.moderation.dependencies as moderation_deps
8
+ import core_framework.domains.post.dependencies as post_deps
9
+ import core_framework.domains.user.dependencies as user_deps
10
+ from core_framework.domains.comment import CommentSubjectType
11
+ from core_framework.domains.post import PostStats, PostStatus, PostWithMetadata
12
+ from core_framework.domains.user import UserIdentity
13
+
14
+
15
+ def _serialize_post(
16
+ post: PostWithMetadata,
17
+ post_stats_mapping: defaultdict[str, PostStats],
18
+ hashtags_mapping: defaultdict[str, list[str]],
19
+ user_identity_mapping: defaultdict[str, UserIdentity],
20
+ ) -> dict[str, Any]:
21
+ stats = post_stats_mapping[post.id]
22
+ hashtags = hashtags_mapping[post.id]
23
+ author_identity = user_identity_mapping[post.author_id]
24
+ return {
25
+ "id": post.id,
26
+ "author": {
27
+ "user_id": post.author_id,
28
+ "username": author_identity.username,
29
+ "display_name": author_identity.display_name,
30
+ },
31
+ "content": post.content,
32
+ "hashtags": hashtags,
33
+ "visibility": post.visibility,
34
+ "status": post.status,
35
+ "edited_count": post.edited_count,
36
+ "stats": {
37
+ "like_count": stats.like_count,
38
+ "comment_count": stats.comment_count,
39
+ "view_count": stats.view_count,
40
+ "report_count": stats.report_count,
41
+ },
42
+ "edited_at": post.edited_at,
43
+ "created_at": post.created_at,
44
+ }
45
+
46
+
47
+ async def retrieve_all_posts(*, cursor: datetime, limit: int) -> list[dict[str, Any]]:
48
+ posts = await post_deps.post_service.retrieve_posts_unfiltered(cursor=cursor, limit=limit)
49
+ post_ids_set = {post.id for post in posts}
50
+ author_ids = {post.author_id for post in posts}
51
+ async with TaskGroup() as tg:
52
+ stats_task = tg.create_task(post_deps.post_service.retrieve_post_stats_mapping(post_ids=post_ids_set))
53
+ hashtags_task = tg.create_task(post_deps.post_service.retrieve_hashtags_mapping(post_ids=post_ids_set))
54
+ identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
55
+ return [
56
+ _serialize_post(
57
+ post,
58
+ stats_task.result(),
59
+ hashtags_task.result(),
60
+ identity_task.result(),
61
+ )
62
+ for post in posts
63
+ ]
64
+
65
+
66
+ async def retrieve_post_by_id(*, post_id: str) -> dict[str, Any]:
67
+ post = await post_deps.post_service.retrieve_post_with_metadata_by_id(post_id=post_id)
68
+ post_ids_set = {post.id}
69
+ author_ids = {post.author_id}
70
+ async with TaskGroup() as tg:
71
+ stats_task = tg.create_task(post_deps.post_service.retrieve_post_stats_mapping(post_ids=post_ids_set))
72
+ hashtags_task = tg.create_task(post_deps.post_service.retrieve_hashtags_mapping(post_ids=post_ids_set))
73
+ identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids=author_ids))
74
+ return _serialize_post(
75
+ post,
76
+ stats_task.result(),
77
+ hashtags_task.result(),
78
+ identity_task.result(),
79
+ )
80
+
81
+
82
+ async def set_post_inactive(*, post_id: str) -> None:
83
+ await post_deps.post_service.set_post_status_by_id(post_id=post_id, status=PostStatus.DELETED)
84
+
85
+
86
+ async def restore_post(post_id: str) -> None:
87
+ await post_deps.post_service.set_post_status_by_id(post_id=post_id, status=PostStatus.ACTIVE)
88
+
89
+
90
+ async def remove_post(*, post_id: str) -> None:
91
+ comment_ids = await comment_deps.comment_service.retrieve_comment_ids_by_subject_strong(
92
+ subject_type=CommentSubjectType.POST,
93
+ subject_id=post_id,
94
+ )
95
+ async with TaskGroup() as tg:
96
+ tg.create_task(moderation_deps.moderation_service.remove_post_reports_by_target_id(target_id=post_id))
97
+ tg.create_task(moderation_deps.moderation_service.remove_comment_reports_by_target_ids(target_ids=comment_ids))
98
+ tg.create_task(
99
+ comment_deps.comment_service.remove_comments_by_subject(
100
+ subject_type=CommentSubjectType.POST,
101
+ subject_id=post_id,
102
+ )
103
+ )
104
+ await post_deps.post_service.delete_post_by_id(post_id=post_id)
@@ -0,0 +1,28 @@
1
+ from asyncio import TaskGroup
2
+
3
+ import core_framework.domains.comment.dependencies as comment_deps
4
+ import core_framework.domains.moderation.dependencies as moderation_deps
5
+ import core_framework.domains.post.dependencies as post_deps
6
+
7
+
8
+ async def aggregate_post_view_counts() -> list[str]:
9
+ return await post_deps.post_service.aggregate_post_view_counts()
10
+
11
+
12
+ async def aggregate_post_stats(*, post_id: str) -> None:
13
+ async with TaskGroup() as tg:
14
+ like_task = tg.create_task(post_deps.post_service.retrieve_post_like_count(post_id=post_id))
15
+ report_task = tg.create_task(moderation_deps.moderation_service.retrieve_post_report_count(post_id=post_id))
16
+ comment_count_task = tg.create_task(comment_deps.comment_service.retrieve_post_comment_count(post_id=post_id))
17
+
18
+ counts = {
19
+ "like_count": like_task.result(),
20
+ "comment_count": comment_count_task.result(),
21
+ "report_count": report_task.result(),
22
+ }
23
+ await post_deps.post_service.update_post_stats(
24
+ post_id=post_id,
25
+ like_count=counts["like_count"],
26
+ comment_count=counts["comment_count"],
27
+ report_count=counts["report_count"],
28
+ )