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,334 @@
1
+ import asyncio
2
+ from collections import defaultdict
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import Any, Final
5
+
6
+ from core_framework.domains.moderation.enums import (
7
+ AppealDecision,
8
+ ModerationActionType,
9
+ ReportCategory,
10
+ RestrictionCategory,
11
+ )
12
+ from core_framework.domains.moderation.exceptions import SelfReportException
13
+ from core_framework.domains.moderation.models import (
14
+ Appeal,
15
+ InternalNote,
16
+ ModerationAction,
17
+ Report,
18
+ RestrictionHistory,
19
+ UserModeration,
20
+ UserRestriction,
21
+ )
22
+ from core_framework.domains.moderation.repository import ModerationRepository
23
+
24
+
25
+ class ModerationService:
26
+ DEFAULT_BAN_EXPIRATION_DAYS: Final[int] = 4000
27
+ DEFAULT_MUTE_EXPIRATION_DAYS: Final[int] = 7
28
+ USER_DETAIL_NOTES_LIMIT: Final[int] = 5
29
+
30
+ def __init__(self, repository: ModerationRepository):
31
+ self.repository = repository
32
+
33
+ # Restrictions
34
+ async def ban_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
35
+ expires_at = datetime.now(timezone.utc) + timedelta(days=self.DEFAULT_BAN_EXPIRATION_DAYS)
36
+ await self.repository.upsert_ban_state(
37
+ actor_id=actor_id,
38
+ user_id=user_id,
39
+ reason=reason,
40
+ category=category,
41
+ expires_at=expires_at,
42
+ )
43
+
44
+ async def mute_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
45
+ expires_at = datetime.now(timezone.utc) + timedelta(days=self.DEFAULT_MUTE_EXPIRATION_DAYS)
46
+ await self.repository.upsert_mute_state(
47
+ actor_id=actor_id,
48
+ user_id=user_id,
49
+ reason=reason,
50
+ category=category,
51
+ expires_at=expires_at,
52
+ )
53
+
54
+ async def warn_user(self, *, actor_id: str, user_id: str, reason: str, category: RestrictionCategory) -> None:
55
+ await self.repository.upsert_warn_state(actor_id=actor_id, user_id=user_id, reason=reason, category=category)
56
+
57
+ async def clear_user_restriction(self, *, actor_id: str, user_id: str) -> None:
58
+ await self.repository.delete_user_restriction(actor_id=actor_id, user_id=user_id)
59
+
60
+ async def retrieve_expired_mute_user_ids(self) -> list[str]:
61
+ return await self.repository.select_expired_mute_user_ids()
62
+
63
+ # Reports
64
+ async def add_user_report(self, *, reporter_id: str, target_id: str, category: ReportCategory, reason: str) -> None:
65
+ if reporter_id == target_id:
66
+ raise SelfReportException()
67
+
68
+ await self.repository.insert_user_report(
69
+ reporter_id=reporter_id,
70
+ target_id=target_id,
71
+ category=category,
72
+ reason=reason,
73
+ )
74
+
75
+ async def retrieve_user_reports(
76
+ self,
77
+ *,
78
+ reporter_id: str | None,
79
+ target_id: str | None,
80
+ cursor: datetime,
81
+ limit: int,
82
+ ) -> list[Report]:
83
+ return await self.repository.select_user_reports(
84
+ reporter_id=reporter_id,
85
+ target_id=target_id,
86
+ cursor=cursor,
87
+ limit=limit,
88
+ )
89
+
90
+ async def remove_user_report(self, *, report_id: int) -> None:
91
+ await self.repository.delete_user_report_by_id(report_id=report_id)
92
+
93
+ async def remove_user_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
94
+ await self.repository.delete_user_report_by_reporter_and_target(
95
+ reporter_id=reporter_id,
96
+ target_id=target_id,
97
+ )
98
+
99
+ async def add_post_report(
100
+ self,
101
+ *,
102
+ reporter_id: str,
103
+ target_id: str,
104
+ category: ReportCategory,
105
+ reason: str,
106
+ ) -> None:
107
+ await self.repository.insert_post_report(
108
+ reporter_id=reporter_id,
109
+ target_id=target_id,
110
+ category=category,
111
+ reason=reason,
112
+ )
113
+
114
+ async def retrieve_post_reports(
115
+ self,
116
+ *,
117
+ reporter_id: str | None,
118
+ target_id: str | None,
119
+ cursor: datetime,
120
+ limit: int,
121
+ ) -> list[Report]:
122
+ return await self.repository.select_post_reports(
123
+ reporter_id=reporter_id,
124
+ target_id=target_id,
125
+ cursor=cursor,
126
+ limit=limit,
127
+ )
128
+
129
+ async def retrieve_comment_reports(
130
+ self,
131
+ *,
132
+ reporter_id: str | None,
133
+ target_id: str | None,
134
+ cursor: datetime,
135
+ limit: int,
136
+ ) -> list[Report]:
137
+ return await self.repository.select_comment_reports(
138
+ reporter_id=reporter_id,
139
+ target_id=target_id,
140
+ cursor=cursor,
141
+ limit=limit,
142
+ )
143
+
144
+ async def retrieve_post_report_count(self, *, post_id: str) -> int:
145
+ return await self.repository.select_post_report_count(post_id=post_id)
146
+
147
+ async def retrieve_comment_report_count(self, *, comment_id: str) -> int:
148
+ return await self.repository.select_comment_report_count(comment_id=comment_id)
149
+
150
+ async def remove_post_report(self, *, report_id: int) -> None:
151
+ await self.repository.delete_post_report_by_id(report_id=report_id)
152
+
153
+ async def remove_comment_report(self, *, report_id: int) -> None:
154
+ await self.repository.delete_comment_report_by_id(report_id=report_id)
155
+
156
+ async def remove_post_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
157
+ await self.repository.delete_post_report_by_reporter_and_target(
158
+ reporter_id=reporter_id,
159
+ target_id=target_id,
160
+ )
161
+
162
+ async def remove_post_reports_by_target_id(self, *, target_id: str) -> None:
163
+ await self.repository.delete_post_reports_by_target_id(target_id=target_id)
164
+
165
+ async def retrieve_post_ids_reported_by_user(self, *, reporter_id: str, post_ids: set[str]) -> frozenset[str]:
166
+ ids = await self.repository.select_post_ids_reported_by_user(reporter_id=reporter_id, post_ids=post_ids)
167
+ return frozenset(ids)
168
+
169
+ async def retrieve_comment_ids_reported_by_user(self, *, reporter_id: str, comment_ids: set[str]) -> frozenset[str]:
170
+ ids = await self.repository.select_comment_ids_reported_by_user(
171
+ reporter_id=reporter_id, comment_ids=comment_ids
172
+ )
173
+ return frozenset(ids)
174
+
175
+ async def add_comment_report(
176
+ self,
177
+ *,
178
+ reporter_id: str,
179
+ target_id: str,
180
+ category: ReportCategory,
181
+ reason: str,
182
+ ) -> None:
183
+ await self.repository.insert_comment_report(
184
+ reporter_id=reporter_id,
185
+ target_id=target_id,
186
+ category=category,
187
+ reason=reason,
188
+ )
189
+
190
+ async def remove_comment_report_by_reporter(self, *, reporter_id: str, target_id: str) -> None:
191
+ await self.repository.delete_comment_report_by_reporter_and_target(
192
+ reporter_id=reporter_id,
193
+ target_id=target_id,
194
+ )
195
+
196
+ async def remove_comment_reports_by_target_ids(self, *, target_ids: set[str]) -> None:
197
+ await self.repository.delete_comment_reports_by_target_ids(target_ids=target_ids)
198
+
199
+ # Appeals
200
+ async def add_appeal(self, *, user_id: str, justification: str) -> None:
201
+ await self.repository.insert_appeal(user_id=user_id, justification=justification)
202
+
203
+ async def decide_appeal(self, *, actor_id: str, appeal_id: int, decision: AppealDecision, reason: str) -> Appeal:
204
+ await self.repository.update_appeal_decision(
205
+ actor_id=actor_id,
206
+ appeal_id=appeal_id,
207
+ decision=decision,
208
+ reason=reason,
209
+ )
210
+ return await self.repository.select_appeal_strong(appeal_id=appeal_id)
211
+
212
+ async def retrieve_appeals(self, *, status: AppealDecision | None, cursor: datetime, limit: int) -> list[Appeal]:
213
+ return await self.repository.select_appeals(status=status, cursor=cursor, limit=limit)
214
+
215
+ async def retrieve_appeals_of_user(
216
+ self,
217
+ *,
218
+ user_id: str,
219
+ status: AppealDecision | None,
220
+ cursor: datetime,
221
+ limit: int,
222
+ ) -> list[Appeal]:
223
+ return await self.repository.select_appeals(user_id=user_id, status=status, cursor=cursor, limit=limit)
224
+
225
+ async def remove_current_appeal_of_user(self, *, user_id: str) -> None:
226
+ await self.repository.delete_current_appeal_of_user(user_id=user_id)
227
+
228
+ # Internal Notes
229
+ async def add_internal_note(
230
+ self,
231
+ *,
232
+ actor_id: str,
233
+ target_user_id: str,
234
+ content: str,
235
+ ) -> InternalNote:
236
+ return await self.repository.insert_internal_note(
237
+ actor_id=actor_id,
238
+ target_user_id=target_user_id,
239
+ content=content,
240
+ )
241
+
242
+ async def retrieve_internal_note_strong(self, *, note_id: int, target_user_id: str) -> InternalNote | None:
243
+ return await self.repository.select_internal_note_by_id_strong(
244
+ note_id=note_id,
245
+ target_user_id=target_user_id,
246
+ )
247
+
248
+ async def retrieve_internal_notes(self, *, target_user_id: str) -> list[InternalNote]:
249
+ return await self.repository.select_internal_notes(target_user_id=target_user_id)
250
+
251
+ async def retrieve_internal_notes_paginated(
252
+ self,
253
+ *,
254
+ target_user_id: str,
255
+ cursor: datetime,
256
+ limit: int,
257
+ ) -> list[InternalNote]:
258
+ return await self.repository.select_internal_notes_paginated(
259
+ target_user_id=target_user_id,
260
+ cursor=cursor,
261
+ limit=limit,
262
+ )
263
+
264
+ async def remove_internal_note(self, *, note_id: int, target_user_id: str) -> None:
265
+ await self.repository.delete_internal_note(note_id=note_id, target_user_id=target_user_id)
266
+
267
+ # User Moderation
268
+ async def retrieve_user_restriction(self, *, user_id: str) -> UserRestriction:
269
+ mapping = await self.repository.select_user_restriction_mapping(user_ids={user_id})
270
+ return mapping.get(user_id, UserRestriction.DEFAULT)
271
+
272
+ async def retrieve_user_restriction_strong(self, *, user_id: str) -> UserRestriction:
273
+ mapping = await self.repository.select_user_restriction_mapping_strong(user_ids={user_id})
274
+ return mapping.get(user_id, UserRestriction.DEFAULT)
275
+
276
+ async def retrieve_user_moderation_mapping(self, *, user_ids: set[str]) -> defaultdict[str, UserModeration]:
277
+ user_restriction_mapping = await self.repository.select_user_restriction_mapping(user_ids=user_ids)
278
+ result: defaultdict[str, UserModeration] = defaultdict(lambda: UserModeration.DEFAULT)
279
+ for user_id, restriction in user_restriction_mapping.items():
280
+ result[user_id] = UserModeration(restriction=restriction, notes=())
281
+ return result
282
+
283
+ async def retrieve_user_moderation_for_detail(self, *, user_id: str) -> UserModeration:
284
+ far_future = datetime(3000, 1, 1, tzinfo=timezone.utc)
285
+ async with asyncio.TaskGroup() as tg:
286
+ restriction_task = tg.create_task(self.repository.select_user_restriction_mapping(user_ids={user_id}))
287
+ notes_task = tg.create_task(
288
+ self.repository.select_internal_notes_paginated(
289
+ target_user_id=user_id,
290
+ cursor=far_future,
291
+ limit=self.USER_DETAIL_NOTES_LIMIT,
292
+ )
293
+ )
294
+ restriction_mapping = restriction_task.result()
295
+ notes = notes_task.result()
296
+ restriction = restriction_mapping.get(user_id, UserRestriction.DEFAULT)
297
+ return UserModeration(restriction=restriction, notes=tuple(notes))
298
+
299
+ async def remove_user(self, *, user_id: str) -> None:
300
+ await self.repository.delete_user(user_id=user_id)
301
+
302
+ async def retrieve_restriction_history(
303
+ self,
304
+ *,
305
+ user_id: str,
306
+ cursor: datetime,
307
+ limit: int,
308
+ ) -> list[RestrictionHistory]:
309
+ return await self.repository.select_restriction_history(user_id=user_id, cursor=cursor, limit=limit)
310
+
311
+ # Moderation Actions
312
+ async def retrieve_moderation_actions_strong(
313
+ self, *, actor_id: str, cursor: datetime, limit: int
314
+ ) -> list[ModerationAction]:
315
+ return await self.repository.select_moderation_actions_strong(
316
+ actor_id=actor_id,
317
+ cursor=cursor,
318
+ limit=limit,
319
+ )
320
+
321
+ async def record_moderation_action(
322
+ self,
323
+ *,
324
+ actor_id: str,
325
+ action_type: ModerationActionType,
326
+ target_user_id: str | None = None,
327
+ action_metadata: dict[str, Any] | None = None,
328
+ ) -> None:
329
+ await self.repository.insert_moderation_action(
330
+ actor_id=actor_id,
331
+ action_type=action_type,
332
+ target_user_id=target_user_id,
333
+ action_metadata=action_metadata,
334
+ )
@@ -0,0 +1,182 @@
1
+ # Post Domain
2
+
3
+ ## Scope
4
+
5
+ Owns post content lifecycle, audience visibility rules, media attachments, post likes, hashtags, mentions, and engagement analytics. Provides the authoritative source for post existence and current content state.
6
+
7
+ ## Owns
8
+
9
+ ### Post Content
10
+
11
+ - Post creation, retrieval, editing, and deletion state
12
+ - Author-owned content fields (body and optional metadata)
13
+ - Post timestamps (created/updated/edited)
14
+ - Edit limit (max 5 edits) and `edited_count` for author capability display; retrieval responses include `author_context` (can_edit, can_delete) when viewer is author—see `docs/flows/posts/author_context.md`
15
+
16
+ ### Post Visibility State
17
+
18
+ - Audience visibility state of a post (public, followers, private, unlisted)
19
+ - Soft-delete state for post lifecycle management
20
+ - Readability/editability rules based on state
21
+
22
+ ### Media Attachments
23
+
24
+ - Post-linked media metadata for images and videos
25
+ - Attachment ordering and per-post attachment limits
26
+ - Media state lifecycle (processing, ready, failed, removed)
27
+
28
+ ### Post Likes
29
+
30
+ - User-to-post like relationships
31
+ - Idempotent like/unlike behavior as relationship state
32
+ - Aggregate like counts derived from post_like rows
33
+
34
+ ### Hashtags and Discovery
35
+
36
+ - Hashtag extraction and canonicalization from post content
37
+ - Hashtag-to-post indexing for discovery and retrieval
38
+ - Search/discovery query support over post content and hashtags
39
+
40
+ ### Mentions
41
+
42
+ - Mention extraction from post content (e.g., @username)
43
+ - Mention-to-post mapping for retrieval and mention inbox use cases
44
+ - Mention events for downstream notification workflows
45
+
46
+ ### Engagement Analytics
47
+
48
+ - Post engagement counters and derived analytics (views, likes, comments, reports)
49
+ - View tracking records and aggregation strategy for post metrics
50
+ - Retrieval of post-level metrics for authors and admins
51
+
52
+ ## Does Not Own
53
+
54
+ - User identity, authentication, or profile data
55
+ - User relationship graph (follows/friends/blocks)
56
+ - Post reports (owned by moderation domain; moderation is the policy enforcement system)
57
+ - Enforcement decisions (ban/mute/warn) and appeal lifecycle - owned by moderation domain
58
+ - Notification delivery mechanisms (domain may emit mention events, but delivery is external)
59
+ - Feed ranking, recommendation, or timeline assembly in API layer
60
+
61
+ ## User removal and redacted authors (design)
62
+
63
+ When an account is removed, **authored posts are not hard-deleted**: **retain** `posts.id`, **redact** content to an agreed placeholder, and set **`author_id`** to the same **sentinel** used for redacted comments (see `docs/flows/users/user_removal.md`). **Post reports and post likes** are kept; **`reporter_id` / `liker_id` are not rewritten** to the sentinel. Public post **GET** responses set **`engagement_allowed`** to `false` for redacted-author posts so clients can hide like/report actions; **write** endpoints do not enforce that flag.
64
+
65
+ ## Hard delete post (design)
66
+
67
+ **Hard delete** is a separate operation from user removal. For `DELETE /admin/posts/{post_id}` the **target is physical removal** of the `posts` row and **all post-domain rows** that reference that `post_id` (e.g. `post_likes`, `post_stats`, `post_attachments`, `post_hashtags`, `post_mentions`, `post_views`), plus **cross-domain cleanup**: every comment on that post (full subtree), their likes/stats/attachments/dirty markers, and **moderation** `post_reports` and the relevant `comment_reports`.
68
+
69
+ Authoritative checklist and HTTP flow notes: `docs/flows/posts/admin_posts.md` (Hard delete post). Keep repository and application delete paths aligned with that contract whenever hard-delete behavior changes.
70
+
71
+ ## Invariants
72
+
73
+ - A post has exactly one author_id
74
+ - A post in deleted state is not editable
75
+ - Attachments are immutable once published on an active post
76
+ - Attachment type must be image or video
77
+ - A user can like a given post at most once
78
+ - Post visibility must be one of (public, followers, private, unlisted)
79
+ - A user can be mentioned at most once per post
80
+ - Engagement counters are non-negative
81
+ - Domain stores only IDs for external entities (no cross-domain foreign keys)
82
+ - Post reads/writes only touch the post schema
83
+
84
+ ## Feed Visibility Filtering Rules
85
+
86
+ - Post domain may keep local lookup mirrors for cross-domain visibility checks:
87
+ - `user_restrictions_lookup` (restricted authors: muted/banned)
88
+ - `user_blocks_lookup` (directed block edge: blocker -> blocked)
89
+ - For viewer `A` and post author `B`, hide the post when either:
90
+ - `A` has blocked `B` (exists in `user_blocks_lookup`)
91
+ - `B` is restricted as muted/banned (exists in `user_restrictions_lookup`)
92
+ - Special rule: if viewer `A` is muted, `A` can still see their own posts.
93
+ - These lookup tables are mirrors for read-time filtering only; source-of-truth remains outside post domain.
94
+
95
+ ## Lookup Sync Contract
96
+
97
+ - Source-of-truth for restriction state is the moderation domain.
98
+ - Source-of-truth for user-to-user blocks is the user domain.
99
+ - Post domain mirrors (`user_restrictions_lookup`, `user_blocks_lookup`) exist only to support read-time filtering.
100
+ - Sync operations should be idempotent and safe to run repeatedly.
101
+ - No-op moderation operations (for example, banning an already banned user or clearing an already active user)
102
+ should still reconcile post-domain lookup rows to heal prior cross-domain partial failures.
103
+
104
+ ## Persistence Model
105
+
106
+ ### Tables
107
+
108
+ #### posts
109
+
110
+ - Canonical post record (author_id, content, visibility/state, timestamps)
111
+ - Supports soft deletion and edit tracking
112
+ - Indexed for recent-first listing and author timeline queries
113
+
114
+ #### post_likes
115
+
116
+ - Tracks user likes on posts as a relationship table
117
+ - Stores liker_id, post_id, and created_at
118
+ - One row per (liker_id, post_id) pair to enforce idempotent likes
119
+
120
+ #### post_stats
121
+
122
+ - Denormalized engagement counters per post
123
+ - Stores post_id, like_count, comment_count, view_count, report_count, view_count_aggregated_at, timestamps
124
+ - like_count, comment_count, report_count from post_likes, comments, moderation domain post_reports; view_count from incremental aggregation of post_views
125
+ - One row per post; created when post is created
126
+ - Aggregated periodically by background worker (every 15 min); see `docs/flows/posts/post_stats_aggregation.md`
127
+
128
+ #### post_attachments
129
+
130
+ - Tracks media attached to posts (image/video)
131
+ - Stores post_id, media_id/storage_key, media_type, order_index, and processing status
132
+ - Supports deterministic ordering for multi-attachment posts
133
+
134
+ #### post_hashtags
135
+
136
+ - Tracks normalized hashtag labels attached to posts
137
+ - Supports hashtag-based retrieval and discovery
138
+ - One row per (post_id, hashtag) pair
139
+
140
+ #### post_mentions
141
+
142
+ - Tracks mentioned users per post
143
+ - Stores post_id, mentioned_user_id, and mention metadata
144
+ - One row per (post_id, mentioned_user_id) pair
145
+
146
+ #### post_views
147
+
148
+ - Append-only view events for analytics
149
+ - Stores post_id, token (deduplication), created_at, and request_context (JSONB)
150
+ - request_context holds optional fields: viewer_id, country_code, client_type, browser_name, os_name, referrer
151
+ - Unique (post_id, token) prevents duplicate views per token
152
+ - Used to derive view_count via incremental aggregation (every 15 min). For each affected post, a task also aggregates like/comment counts (from post_likes, comments) and report counts (from moderation domain post_reports)
153
+
154
+ #### user_restrictions_lookup
155
+
156
+ - Read-time mirror for restricted users (muted/banned). Source-of-truth: moderation domain.
157
+ - Stores user_id, restriction_type (muted, banned)
158
+ - Used in feed queries to hide posts from restricted authors. Sync operations reconcile from moderation domain; see Lookup Sync Contract above.
159
+
160
+ #### user_blocks_lookup
161
+
162
+ - Read-time mirror for user-to-user blocks. Source-of-truth: user domain.
163
+ - Stores blocker_id, blocked_id (directed edge: blocker has blocked blocked)
164
+ - Used in feed queries to hide posts from authors the viewer has blocked. Sync operations reconcile from user domain; see Lookup Sync Contract above.
165
+
166
+ ### Enums
167
+
168
+ #### post_status
169
+
170
+ - Lifecycle state of a post (active, deleted)
171
+
172
+ #### post_visibility
173
+
174
+ - Post visibility state (public, followers, private, unlisted)
175
+
176
+ #### post_media_type
177
+
178
+ - Attachment media type (image, video)
179
+
180
+ #### post_attachment_status
181
+
182
+ - Attachment processing lifecycle (processing, ready, failed, removed)
@@ -0,0 +1,22 @@
1
+ from core_framework.domains.post.enums import AttachmentType, PostStatus, PostVisibility
2
+ from core_framework.domains.post.exceptions import (
3
+ BasePostException,
4
+ EditLimitReachedException,
5
+ PostNotFoundException,
6
+ PostUpdateNotFoundException,
7
+ )
8
+ from core_framework.domains.post.models import Post, PostPreview, PostStats, PostWithMetadata
9
+
10
+ __all__ = [
11
+ "AttachmentType",
12
+ "PostStatus",
13
+ "PostVisibility",
14
+ "BasePostException",
15
+ "EditLimitReachedException",
16
+ "Post",
17
+ "PostNotFoundException",
18
+ "PostPreview",
19
+ "PostStats",
20
+ "PostUpdateNotFoundException",
21
+ "PostWithMetadata",
22
+ ]
@@ -0,0 +1,3 @@
1
+ from typing import Final
2
+
3
+ REDACTED_CONTENT_PLACEHOLDER: Final[str] = "<deleted>"
@@ -0,0 +1,29 @@
1
+ from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
2
+ from core_framework.domains.post.repository import PostRepository
3
+ from core_framework.domains.post.service import PostService
4
+
5
+
6
+ def build_post_repository(runtime: CoreRuntime) -> PostRepository:
7
+ return PostRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
8
+
9
+
10
+ def build_post_service(runtime: CoreRuntime) -> PostService:
11
+ return PostService(build_post_repository(runtime))
12
+
13
+
14
+ def configure_post_dependencies(runtime: CoreRuntime) -> None:
15
+ global post_repository, post_service
16
+ post_repository = build_post_repository(runtime)
17
+ post_service = build_post_service(runtime)
18
+
19
+
20
+ post_repository = unconfigured_dependency("PostRepository")
21
+ post_service = unconfigured_dependency("PostService")
22
+
23
+ __all__ = [
24
+ "build_post_repository",
25
+ "build_post_service",
26
+ "configure_post_dependencies",
27
+ "post_repository",
28
+ "post_service",
29
+ ]
@@ -0,0 +1,18 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class PostVisibility(StrEnum):
5
+ PUBLIC = "public"
6
+ FOLLOWERS = "followers"
7
+ PRIVATE = "private"
8
+ UNLISTED = "unlisted"
9
+
10
+
11
+ class AttachmentType(StrEnum):
12
+ IMAGE = "image"
13
+ VIDEO = "video"
14
+
15
+
16
+ class PostStatus(StrEnum):
17
+ ACTIVE = "active"
18
+ DELETED = "deleted"
@@ -0,0 +1,21 @@
1
+ class BasePostException(Exception):
2
+ message: str
3
+
4
+ def __init__(self, message: str):
5
+ self.message = message
6
+ super().__init__(self.message)
7
+
8
+
9
+ class PostUpdateNotFoundException(BasePostException):
10
+ def __init__(self) -> None:
11
+ super().__init__("Unable to process request")
12
+
13
+
14
+ class EditLimitReachedException(BasePostException):
15
+ def __init__(self) -> None:
16
+ super().__init__("Edit limit reached. Maximum edits allowed per post.")
17
+
18
+
19
+ class PostNotFoundException(BasePostException):
20
+ def __init__(self) -> None:
21
+ super().__init__("Post not found")
@@ -0,0 +1,53 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import ClassVar
4
+
5
+ from core_framework.domains.post.enums import PostStatus, PostVisibility
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Post:
10
+ id: str
11
+ author_id: str
12
+ content: str
13
+ visibility: PostVisibility
14
+ edited_count: int
15
+ edited_at: datetime | None
16
+ created_at: datetime
17
+
18
+
19
+ @dataclass(frozen=True, slots=True, kw_only=True)
20
+ class PostWithMetadata:
21
+ id: str
22
+ author_id: str
23
+ content: str
24
+ visibility: PostVisibility
25
+ status: PostStatus
26
+ edited_count: int
27
+ edited_at: datetime | None
28
+ created_at: datetime
29
+
30
+
31
+ @dataclass(frozen=True, slots=True, kw_only=True)
32
+ class PostPreview:
33
+ id: str
34
+ author_id: str
35
+ content: str
36
+
37
+
38
+ @dataclass(frozen=True, slots=True, kw_only=True)
39
+ class PostStats:
40
+ like_count: int
41
+ comment_count: int
42
+ view_count: int
43
+ report_count: int
44
+
45
+ DEFAULT: ClassVar[PostStats]
46
+
47
+
48
+ PostStats.DEFAULT = PostStats(
49
+ like_count=0,
50
+ comment_count=0,
51
+ view_count=0,
52
+ report_count=0,
53
+ )