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,259 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+ from typing import Any, Final, Literal
4
+
5
+ from ulid import ULID
6
+
7
+ from core_framework.domains.comment import Comment, CommentPreview, CommentStats, CommentStatus, CommentWithMetadata
8
+ from core_framework.domains.comment.enums import CommentSubjectType
9
+ from core_framework.domains.comment.repository import CommentRepository
10
+
11
+
12
+ class CommentService:
13
+ MAX_EDIT_COUNT: Final[int] = 5
14
+ MAX_REPLY_LEVEL: Final[int] = 4
15
+
16
+ def __init__(self, repository: CommentRepository):
17
+ self.repository = repository
18
+
19
+ async def add_comment(
20
+ self,
21
+ *,
22
+ author_id: str,
23
+ content: str,
24
+ subject_type: CommentSubjectType,
25
+ subject_id: str,
26
+ ) -> None:
27
+ await self.repository.insert_comment(
28
+ comment_id=str(ULID()),
29
+ author_id=author_id,
30
+ content=content,
31
+ subject_type=subject_type,
32
+ subject_id=subject_id,
33
+ )
34
+
35
+ async def add_reply(
36
+ self,
37
+ *,
38
+ author_id: str,
39
+ content: str,
40
+ parent_comment_id: str,
41
+ ) -> None:
42
+ await self.repository.insert_reply(
43
+ comment_id=str(ULID()),
44
+ author_id=author_id,
45
+ content=content,
46
+ parent_comment_id=parent_comment_id,
47
+ max_reply_level=self.MAX_REPLY_LEVEL,
48
+ )
49
+
50
+ async def retrieve_comment_content_mapping(self, *, comment_ids: set[str]) -> defaultdict[str, str]:
51
+ comment_content_mapping = await self.repository.select_comment_content_mapping(comment_ids=comment_ids)
52
+ return defaultdict(str, comment_content_mapping)
53
+
54
+ async def retrieve_comments_with_metadata(
55
+ self,
56
+ *,
57
+ cursor: datetime,
58
+ limit: int,
59
+ ) -> list[CommentWithMetadata]:
60
+ return await self.repository.select_comments_with_metadata(cursor=cursor, limit=limit)
61
+
62
+ async def retrieve_comment_with_metadata_by_id(self, *, comment_id: str) -> CommentWithMetadata:
63
+ return await self.repository.select_comment_with_metadata_by_id(comment_id=comment_id)
64
+
65
+ async def retrieve_comment_preview_mapping(self, *, comment_ids: set[str]) -> dict[str, CommentPreview]:
66
+ return await self.repository.select_comment_preview_mapping(comment_ids=comment_ids)
67
+
68
+ async def retrieve_post_comment_count(self, *, post_id: str) -> int:
69
+ return await self.repository.select_active_comment_count_for_post(post_id=post_id)
70
+
71
+ async def retrieve_comments_by_subject(
72
+ self,
73
+ *,
74
+ subject_type: CommentSubjectType,
75
+ subject_id: str,
76
+ cursor: datetime,
77
+ limit: int,
78
+ viewer_id: str | None,
79
+ ) -> list[Comment]:
80
+ return await self.repository.select_comments_by_subject(
81
+ subject_type=subject_type,
82
+ subject_id=subject_id,
83
+ cursor=cursor,
84
+ limit=limit,
85
+ viewer_id=viewer_id,
86
+ )
87
+
88
+ async def retrieve_comments_by_author_id(
89
+ self,
90
+ *,
91
+ author_id: str,
92
+ cursor: datetime,
93
+ limit: int,
94
+ viewer_id: str | None,
95
+ ) -> list[Comment]:
96
+ return await self.repository.select_comments_by_author_id(
97
+ author_id=author_id,
98
+ cursor=cursor,
99
+ limit=limit,
100
+ viewer_id=viewer_id,
101
+ )
102
+
103
+ async def retrieve_replies_by_parent_id(
104
+ self,
105
+ *,
106
+ parent_comment_id: str,
107
+ cursor: datetime,
108
+ limit: int,
109
+ viewer_id: str | None,
110
+ ) -> list[Comment]:
111
+ return await self.repository.select_replies_by_parent_id(
112
+ parent_comment_id=parent_comment_id,
113
+ cursor=cursor,
114
+ limit=limit,
115
+ viewer_id=viewer_id,
116
+ )
117
+
118
+ async def retrieve_comment_by_id(
119
+ self,
120
+ *,
121
+ comment_id: str,
122
+ viewer_id: str | None,
123
+ ) -> Comment | None:
124
+ return await self.repository.select_comment_by_id(
125
+ comment_id=comment_id,
126
+ viewer_id=viewer_id,
127
+ )
128
+
129
+ async def add_user_restriction_lookup(self, *, user_id: str, restriction_type: Literal["muted", "banned"]) -> None:
130
+ await self.repository.insert_user_restriction_lookup(
131
+ user_id=user_id,
132
+ restriction_type=restriction_type,
133
+ )
134
+
135
+ async def remove_user_restriction_lookup(self, *, user_id: str) -> None:
136
+ await self.repository.delete_user_restriction_lookup(user_id=user_id)
137
+
138
+ async def add_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
139
+ await self.repository.insert_user_block_lookup(
140
+ blocker_id=blocker_id,
141
+ blocked_id=blocked_id,
142
+ )
143
+
144
+ async def remove_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
145
+ await self.repository.delete_user_block_lookup(
146
+ blocker_id=blocker_id,
147
+ blocked_id=blocked_id,
148
+ )
149
+
150
+ async def remove_user(self, *, user_id: str) -> None:
151
+ await self.repository.delete_user(user_id=user_id)
152
+
153
+ async def add_comment_like(self, *, comment_id: str, liker_id: str) -> None:
154
+ await self.repository.insert_comment_like(
155
+ comment_id=comment_id,
156
+ liker_id=liker_id,
157
+ )
158
+
159
+ async def remove_comment_like(self, *, comment_id: str, liker_id: str) -> None:
160
+ await self.repository.delete_comment_like(
161
+ comment_id=comment_id,
162
+ liker_id=liker_id,
163
+ )
164
+
165
+ async def edit_comment(
166
+ self,
167
+ *,
168
+ comment_id: str,
169
+ author_id: str,
170
+ validated_update_request: dict[str, Any],
171
+ ) -> None:
172
+ await self.repository.update_comment(
173
+ comment_id=comment_id,
174
+ author_id=author_id,
175
+ comment_updates=validated_update_request,
176
+ max_edit_count=self.MAX_EDIT_COUNT,
177
+ )
178
+
179
+ async def remove_comment(self, *, comment_id: str, author_id: str) -> str | None:
180
+ return await self.repository.update_comment_status_by_id_and_author(
181
+ comment_id=comment_id,
182
+ author_id=author_id,
183
+ status=CommentStatus.DELETED,
184
+ )
185
+
186
+ async def set_comment_status_by_id(self, *, comment_id: str, status: CommentStatus) -> str | None:
187
+ return await self.repository.update_comment_status_by_id(
188
+ comment_id=comment_id,
189
+ status=status,
190
+ )
191
+
192
+ async def retrieve_comment_ids_by_subject_strong(
193
+ self,
194
+ *,
195
+ subject_type: CommentSubjectType,
196
+ subject_id: str,
197
+ ) -> set[str]:
198
+ return await self.repository.select_comment_ids_by_subject_strong(
199
+ subject_type=subject_type,
200
+ subject_id=subject_id,
201
+ )
202
+
203
+ async def retrieve_comment_subtree_ids_strong(self, *, root_comment_id: str) -> set[str]:
204
+ return await self.repository.select_comment_subtree_ids_strong(root_comment_id=root_comment_id)
205
+
206
+ async def retrieve_parent_comment_id_strong(self, *, comment_id: str) -> str | None:
207
+ return await self.repository.select_parent_comment_id_strong(comment_id=comment_id)
208
+
209
+ async def remove_comment_subtree(self, *, root_comment_id: str) -> None:
210
+ await self.repository.delete_comment_subtree(root_comment_id=root_comment_id)
211
+
212
+ async def remove_comments_by_subject(
213
+ self,
214
+ *,
215
+ subject_type: CommentSubjectType,
216
+ subject_id: str,
217
+ ) -> None:
218
+ await self.repository.delete_comments_by_subject(
219
+ subject_type=subject_type,
220
+ subject_id=subject_id,
221
+ )
222
+
223
+ async def retrieve_comment_ids_liked_by_user(self, *, liker_id: str, comment_ids: set[str]) -> frozenset[str]:
224
+ ids = await self.repository.select_comment_ids_liked_by_user(liker_id=liker_id, comment_ids=comment_ids)
225
+ return frozenset(ids)
226
+
227
+ async def retrieve_comment_stats_mapping(self, *, comment_ids: set[str]) -> defaultdict[str, CommentStats]:
228
+ comment_stats_mapping = await self.repository.select_comment_stats_mapping(comment_ids=comment_ids)
229
+ return defaultdict(lambda: CommentStats.DEFAULT, comment_stats_mapping)
230
+
231
+ async def insert_comment_stats_dirty(self, *, comment_id: str) -> None:
232
+ await self.repository.insert_comment_stats_dirty(comment_id=comment_id)
233
+
234
+ async def claim_comment_ids_dirty(self) -> list[str]:
235
+ return await self.repository.claim_comment_ids_dirty()
236
+
237
+ async def delete_comment_stats_dirty(self, *, comment_ids: set[str]) -> None:
238
+ await self.repository.delete_comment_stats_dirty(comment_ids=comment_ids)
239
+
240
+ async def update_comment_stats(
241
+ self,
242
+ *,
243
+ comment_id: str,
244
+ like_count: int,
245
+ reply_count: int,
246
+ report_count: int,
247
+ ) -> None:
248
+ await self.repository.update_comment_stats(
249
+ comment_id=comment_id,
250
+ like_count=like_count,
251
+ reply_count=reply_count,
252
+ report_count=report_count,
253
+ )
254
+
255
+ async def retrieve_comment_like_count(self, *, comment_id: str) -> int:
256
+ return await self.repository.select_comment_like_count(comment_id=comment_id)
257
+
258
+ async def retrieve_comment_reply_count(self, *, comment_id: str) -> int:
259
+ return await self.repository.select_comment_reply_count(comment_id=comment_id)
@@ -0,0 +1,138 @@
1
+ # Moderation Domain
2
+
3
+ ## Scope
4
+
5
+ Moderation is the **policy enforcement system**. It owns report intake (user, post, comment), review workflow, enforcement actions (restrictions), appeals, and audit. Provides the authoritative source for whether a user is restricted and why.
6
+
7
+ **Policy enforcement lifecycle**: Intake (reports) → Review (moderation queue) → Enforcement (restrictions) → Appeals → Audit (history and actions).
8
+
9
+ ## Owns
10
+
11
+ ### Reports
12
+
13
+ - **User reports**: User-to-user reports for policy violations
14
+ - **Post reports**: User-to-post reports for policy violations
15
+ - **Comment reports**: User-to-comment reports for policy violations
16
+ - Report reason/category and freeform details
17
+ - Reports are for analytics and moderation intake; reporters do not receive outcome notifications
18
+
19
+ ### Restriction History
20
+
21
+ - Append-only audit log of all restriction changes
22
+ - Tracks insert, update, and delete operations on restrictions
23
+ - Each entry includes actor, target, action type, and restriction details
24
+ - Entries are immutable once created
25
+
26
+ ### Restrictions
27
+
28
+ - Current effective restriction state for each user (active, muted, banned)
29
+ - Derived from enforcement actions or set directly by moderators
30
+ - Includes reason and expiration (if applicable)
31
+
32
+ ### Appeals
33
+
34
+ - User-initiated appeals (can only be submitted when muted or banned)
35
+ - Appeal status lifecycle (pending, approved, denied)
36
+ - User justification and admin decision reasoning
37
+ - One pending appeal per user at a time
38
+
39
+ ### Audit Trail
40
+
41
+ - All restriction changes are logged via restriction_history
42
+ - Includes actor, target, action type, timestamp, and restriction details
43
+
44
+ ### Moderation Actions
45
+
46
+ - Append-only audit log of all moderation actions (ban, mute, warn, unrestrict, add_note, change_username, delete_user, etc.)
47
+ - Per-moderator retrieval for accountability; see [Moderation Flow: Moderator Actions](../../../docs/flows/moderation/moderator_actions.md)
48
+
49
+ ## Does Not Own
50
+
51
+ - User identity, authentication, or profile data
52
+ - User-to-user blocks (personal preference, owned by user domain)
53
+ - Notification delivery mechanisms
54
+ - Post/comment content and lifecycle (post and comment domains own content; moderation owns reports and enforcement; content domains perform removal when moderators decide)
55
+
56
+ ## Cross-Domain Sync
57
+
58
+ - Moderation remains the source-of-truth for user restriction state.
59
+ - Post-domain `user_restrictions_lookup` is a read-time mirror maintained from moderation application flows.
60
+ - Restriction commands are idempotent and may run on no-op paths to reconcile mirror drift after partial failures.
61
+
62
+ ## Invariants
63
+
64
+ - A user can have at most one active restriction at a time
65
+ - Restriction history entries are append-only and immutable
66
+ - Appeals can only be submitted when user is muted or banned
67
+ - Appeals become read-only once decided (approved or denied)
68
+ - Only users with moderator or admin roles can modify restrictions
69
+ - Users can submit new appeals immediately after denial (only one pending appeal at a time)
70
+
71
+ ## Persistence Model
72
+
73
+ ### Tables
74
+
75
+ #### user_reports
76
+
77
+ - Tracks user-to-user reports for policy violations
78
+ - Stores reporter_id, target_id, category, reason, and timestamp
79
+ - One report per (reporter_id, target_id); duplicate reports are idempotent (no-op)
80
+ - Reports are append-only and used for analytics; no status tracking
81
+
82
+ #### post_reports
83
+
84
+ - Tracks user-to-post reports for policy violations
85
+ - Stores reporter_id, target_id (post ID), category, reason, and timestamp
86
+ - One report per (reporter_id, target_id); duplicate reports are idempotent (no-op)
87
+ - Append-only; used for moderation intake and analytics
88
+
89
+ #### comment_reports
90
+
91
+ - Tracks user-to-comment reports for policy violations
92
+ - Stores reporter_id, target_id (comment ID), category, reason, and timestamp
93
+ - Append-only; used for moderation intake and analytics
94
+
95
+ #### restriction_history
96
+
97
+ - Append-only audit log of all changes to user_restrictions
98
+ - Tracks action type (inserted, updated, deleted) and restriction details
99
+ - Each entry references the target user and acting moderator
100
+ - Populated automatically via trigger on user_restrictions
101
+
102
+ #### user_restrictions
103
+
104
+ - Current effective restriction state per user
105
+ - Stores restriction type (warned, muted, banned), reason, and expiry
106
+ - Updated when new enforcement actions are applied
107
+ - A user with no restriction has no row (or state = active)
108
+
109
+ #### appeals
110
+
111
+ - User appeals submitted when muted or banned
112
+ - Stores user justification and admin decision reason
113
+ - Tracks appeal status and reviewer
114
+ - Rows become read-only once decided (enforced by trigger)
115
+
116
+ #### moderation_actions
117
+
118
+ - Append-only audit log of all moderation actions (ban, mute, warn, add_note, delete_user, etc.)
119
+ - Stores actor_id, action_type, target_user_id (optional), action_metadata, created_at
120
+ - Rows are never deleted when actors or targets are deleted (audit trail preserved)
121
+
122
+ ### Enums
123
+
124
+ #### history_action
125
+
126
+ - Types of history entries (inserted, updated, deleted)
127
+
128
+ #### restriction_type
129
+
130
+ - Types of restrictions (warned, muted, banned)
131
+
132
+ #### appeal_status
133
+
134
+ - Appeal lifecycle states (pending, approved, denied)
135
+
136
+ #### report_category
137
+
138
+ - Report categories used for policy classification (shared by user_reports, post_reports, comment_reports)
@@ -0,0 +1,47 @@
1
+ from core_framework.domains.moderation.enums import (
2
+ AppealDecision,
3
+ HistoryAction,
4
+ ModerationActionType,
5
+ ReportCategory,
6
+ ReportType,
7
+ RestrictionCategory,
8
+ RestrictionType,
9
+ )
10
+ from core_framework.domains.moderation.exceptions import (
11
+ AppealAlreadyDecidedException,
12
+ AppealNotFoundException,
13
+ AppealRequirementException,
14
+ BaseModerationException,
15
+ ExistingPendingAppealException,
16
+ SelfReportException,
17
+ )
18
+ from core_framework.domains.moderation.models import (
19
+ InternalNote,
20
+ ModerationAction,
21
+ Report,
22
+ RestrictionHistory,
23
+ UserModeration,
24
+ UserRestriction,
25
+ )
26
+
27
+ __all__ = [
28
+ "ModerationActionType",
29
+ "ReportType",
30
+ "RestrictionType",
31
+ "AppealDecision",
32
+ "ReportCategory",
33
+ "RestrictionCategory",
34
+ "HistoryAction",
35
+ "Report",
36
+ "UserModeration",
37
+ "RestrictionHistory",
38
+ "InternalNote",
39
+ "ModerationAction",
40
+ "AppealAlreadyDecidedException",
41
+ "AppealNotFoundException",
42
+ "SelfReportException",
43
+ "ExistingPendingAppealException",
44
+ "AppealRequirementException",
45
+ "BaseModerationException",
46
+ "UserRestriction",
47
+ ]
@@ -0,0 +1,29 @@
1
+ from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
2
+ from core_framework.domains.moderation.repository import ModerationRepository
3
+ from core_framework.domains.moderation.service import ModerationService
4
+
5
+
6
+ def build_moderation_repository(runtime: CoreRuntime) -> ModerationRepository:
7
+ return ModerationRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
8
+
9
+
10
+ def build_moderation_service(runtime: CoreRuntime) -> ModerationService:
11
+ return ModerationService(build_moderation_repository(runtime))
12
+
13
+
14
+ def configure_moderation_dependencies(runtime: CoreRuntime) -> None:
15
+ global moderation_repository, moderation_service
16
+ moderation_repository = build_moderation_repository(runtime)
17
+ moderation_service = build_moderation_service(runtime)
18
+
19
+
20
+ moderation_repository = unconfigured_dependency("ModerationRepository")
21
+ moderation_service = unconfigured_dependency("ModerationService")
22
+
23
+ __all__ = [
24
+ "build_moderation_repository",
25
+ "build_moderation_service",
26
+ "configure_moderation_dependencies",
27
+ "moderation_repository",
28
+ "moderation_service",
29
+ ]
@@ -0,0 +1,62 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class RestrictionType(StrEnum):
5
+ ACTIVE = "active"
6
+ WARNED = "warned"
7
+ MUTED = "muted"
8
+ BANNED = "banned"
9
+
10
+
11
+ class ReportType(StrEnum):
12
+ USER = "user"
13
+ POST = "post"
14
+ COMMENT = "comment"
15
+
16
+
17
+ class ReportCategory(StrEnum):
18
+ SPAM = "spam"
19
+ HARASSMENT = "harassment"
20
+ INAPPROPRIATE_CONTENT = "inappropriate_content"
21
+ IMPERSONATION = "impersonation"
22
+ SELF_HARM = "self_harm"
23
+ HATE_SPEECH = "hate_speech"
24
+ VIOLENCE = "violence"
25
+ SCAM = "scam"
26
+ OTHER = "other"
27
+
28
+
29
+ class RestrictionCategory(StrEnum):
30
+ SPAM = "spam"
31
+ HARASSMENT = "harassment"
32
+ INAPPROPRIATE_CONTENT = "inappropriate_content"
33
+ IMPERSONATION = "impersonation"
34
+ HARMFUL_CONTENT = "harmful_content"
35
+ SCAM = "scam"
36
+ OTHER = "other"
37
+
38
+
39
+ class HistoryAction(StrEnum):
40
+ INSERTED = "inserted"
41
+ UPDATED = "updated"
42
+ DELETED = "deleted"
43
+
44
+
45
+ class AppealDecision(StrEnum):
46
+ PENDING = "pending"
47
+ APPROVED = "approved"
48
+ DENIED = "denied"
49
+
50
+
51
+ class ModerationActionType(StrEnum):
52
+ BAN = "ban"
53
+ MUTE = "mute"
54
+ WARN = "warn"
55
+ UNRESTRICT = "unrestrict"
56
+ ADD_NOTE = "add_note"
57
+ DELETE_NOTE = "delete_note"
58
+ CHANGE_USERNAME = "change_username"
59
+ CHANGE_ROLE = "change_role"
60
+ CHANGE_PROFILE = "change_profile"
61
+ DECIDE_APPEAL = "decide_appeal"
62
+ DELETE_USER = "delete_user"
@@ -0,0 +1,31 @@
1
+ class BaseModerationException(Exception):
2
+ message: str
3
+
4
+ def __init__(self, message: str):
5
+ self.message = message
6
+ super().__init__(self.message)
7
+
8
+
9
+ class SelfReportException(BaseModerationException):
10
+ def __init__(self):
11
+ super().__init__("Self report is not allowed")
12
+
13
+
14
+ class ExistingPendingAppealException(BaseModerationException):
15
+ def __init__(self):
16
+ super().__init__("An existing pending appeal already exists for this user")
17
+
18
+
19
+ class AppealNotFoundException(BaseModerationException):
20
+ def __init__(self):
21
+ super().__init__("Appeal not found")
22
+
23
+
24
+ class AppealAlreadyDecidedException(BaseModerationException):
25
+ def __init__(self):
26
+ super().__init__("Appeal already decided")
27
+
28
+
29
+ class AppealRequirementException(BaseModerationException):
30
+ def __init__(self):
31
+ super().__init__("User must be muted or banned to submit an appeal")
@@ -0,0 +1,94 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import ClassVar
4
+
5
+ from core_framework.domains.moderation.enums import (
6
+ AppealDecision,
7
+ HistoryAction,
8
+ ModerationActionType,
9
+ ReportCategory,
10
+ RestrictionCategory,
11
+ RestrictionType,
12
+ )
13
+ from core_framework.domains.user import DEFAULT_USER_ID
14
+
15
+
16
+ @dataclass(frozen=True, slots=True, kw_only=True)
17
+ class Report:
18
+ id: int
19
+ reporter_id: str
20
+ target_id: str
21
+ category: ReportCategory
22
+ reason: str
23
+ created_at: datetime
24
+
25
+
26
+ @dataclass(frozen=True, slots=True, kw_only=True)
27
+ class UserRestriction:
28
+ user_id: str
29
+ status: RestrictionType
30
+ category: RestrictionCategory
31
+ expires_at: datetime | None
32
+
33
+ DEFAULT: ClassVar[UserRestriction]
34
+
35
+
36
+ UserRestriction.DEFAULT = UserRestriction(
37
+ user_id=DEFAULT_USER_ID,
38
+ status=RestrictionType.ACTIVE,
39
+ category=RestrictionCategory.OTHER,
40
+ expires_at=None,
41
+ )
42
+
43
+
44
+ @dataclass(frozen=True, slots=True, kw_only=True)
45
+ class InternalNote:
46
+ id: int
47
+ target_user_id: str
48
+ actor_id: str
49
+ content: str
50
+ created_at: datetime
51
+
52
+
53
+ @dataclass(frozen=True, slots=True, kw_only=True)
54
+ class UserModeration:
55
+ restriction: UserRestriction
56
+ notes: tuple[InternalNote, ...]
57
+
58
+ DEFAULT: ClassVar[UserModeration]
59
+
60
+
61
+ UserModeration.DEFAULT = UserModeration(restriction=UserRestriction.DEFAULT, notes=())
62
+
63
+
64
+ @dataclass(frozen=True, slots=True, kw_only=True)
65
+ class Appeal:
66
+ id: int
67
+ user_id: str
68
+ justification: str
69
+ decision_reason: str | None
70
+ reviewer_id: str | None
71
+ status: AppealDecision
72
+ created_at: datetime
73
+ updated_at: datetime
74
+
75
+
76
+ @dataclass(frozen=True, slots=True, kw_only=True)
77
+ class RestrictionHistory:
78
+ user_id: str
79
+ action: HistoryAction
80
+ restriction_type: RestrictionType
81
+ category: RestrictionCategory
82
+ reason: str
83
+ actor_id: str | None
84
+ created_at: datetime
85
+
86
+
87
+ @dataclass(frozen=True, slots=True, kw_only=True)
88
+ class ModerationAction:
89
+ id: int
90
+ actor_id: str
91
+ action_type: ModerationActionType
92
+ target_user_id: str | None
93
+ action_metadata: dict
94
+ created_at: datetime