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,204 @@
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.post.enums import PostStatus, PostVisibility
8
+ from core_framework.domains.post.exceptions import PostNotFoundException
9
+ from core_framework.domains.post.models import Post, PostPreview, PostStats, PostWithMetadata
10
+ from core_framework.domains.post.repository import PostRepository
11
+
12
+
13
+ class PostService:
14
+ MAX_EDIT_COUNT: Final[int] = 5
15
+
16
+ def __init__(self, repository: PostRepository):
17
+ self.repository = repository
18
+
19
+ async def add_post(
20
+ self,
21
+ *,
22
+ author_id: str,
23
+ content: str,
24
+ visibility: PostVisibility,
25
+ hashtags: list[str],
26
+ ) -> None:
27
+ await self.repository.insert_post(
28
+ post_id=str(ULID()),
29
+ author_id=author_id,
30
+ content=content,
31
+ visibility=visibility,
32
+ hashtags=hashtags,
33
+ )
34
+
35
+ async def edit_post(
36
+ self,
37
+ *,
38
+ post_id: str,
39
+ author_id: str,
40
+ validated_update_request: dict[str, Any],
41
+ ) -> None:
42
+ hashtags = validated_update_request.get("hashtags")
43
+ post_updates = {k: v for k, v in validated_update_request.items() if k != "hashtags"}
44
+ await self.repository.update_post(
45
+ post_id=post_id,
46
+ author_id=author_id,
47
+ post_updates=post_updates,
48
+ hashtags=hashtags,
49
+ max_edit_count=self.MAX_EDIT_COUNT,
50
+ )
51
+
52
+ async def set_post_status_for_author(
53
+ self,
54
+ *,
55
+ post_id: str,
56
+ author_id: str,
57
+ status: PostStatus,
58
+ ) -> None:
59
+ await self.repository.update_post_status_by_id_and_author(
60
+ post_id=post_id,
61
+ author_id=author_id,
62
+ status=status,
63
+ )
64
+
65
+ async def add_user_restriction_lookup(self, *, user_id: str, restriction_type: Literal["muted", "banned"]) -> None:
66
+ await self.repository.insert_user_restriction_lookup(
67
+ user_id=user_id,
68
+ restriction_type=restriction_type,
69
+ )
70
+
71
+ async def remove_user_restriction_lookup(self, *, user_id: str) -> None:
72
+ await self.repository.delete_user_restriction_lookup(user_id=user_id)
73
+
74
+ async def add_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
75
+ await self.repository.insert_user_block_lookup(
76
+ blocker_id=blocker_id,
77
+ blocked_id=blocked_id,
78
+ )
79
+
80
+ async def remove_user_block_lookup(self, *, blocker_id: str, blocked_id: str) -> None:
81
+ await self.repository.delete_user_block_lookup(
82
+ blocker_id=blocker_id,
83
+ blocked_id=blocked_id,
84
+ )
85
+
86
+ async def remove_user(self, *, user_id: str) -> None:
87
+ await self.repository.delete_user(user_id=user_id)
88
+
89
+ async def add_post_like(self, *, post_id: str, liker_id: str) -> None:
90
+ await self.repository.insert_post_like(post_id=post_id, liker_id=liker_id)
91
+
92
+ async def remove_post_like(self, *, post_id: str, liker_id: str) -> None:
93
+ await self.repository.delete_post_like(post_id=post_id, liker_id=liker_id)
94
+
95
+ async def retrieve_post_like_count(self, *, post_id: str) -> int:
96
+ return await self.repository.select_post_like_count(post_id=post_id)
97
+
98
+ async def retrieve_post_ids_liked_by_user(self, *, liker_id: str, post_ids: set[str]) -> frozenset[str]:
99
+ ids = await self.repository.select_post_ids_liked_by_user(liker_id=liker_id, post_ids=post_ids)
100
+ return frozenset(ids)
101
+
102
+ async def add_post_view(
103
+ self,
104
+ *,
105
+ post_id: str,
106
+ token: str,
107
+ request_context: dict[str, str],
108
+ ) -> None:
109
+ await self.repository.insert_post_view(
110
+ post_id=post_id,
111
+ token=token,
112
+ request_context=request_context,
113
+ )
114
+
115
+ async def aggregate_post_view_counts(self) -> list[str]:
116
+ return await self.repository.aggregate_post_view_counts()
117
+
118
+ async def update_post_stats(
119
+ self,
120
+ *,
121
+ post_id: str,
122
+ like_count: int,
123
+ comment_count: int,
124
+ report_count: int,
125
+ ) -> None:
126
+ await self.repository.update_post_stats(
127
+ post_id=post_id,
128
+ like_count=like_count,
129
+ comment_count=comment_count,
130
+ report_count=report_count,
131
+ )
132
+
133
+ async def retrieve_post_content_mapping(self, *, post_ids: set[str]) -> defaultdict[str, str]:
134
+ post_content_mapping = await self.repository.select_post_content_mapping(post_ids=post_ids)
135
+ return defaultdict(str, post_content_mapping)
136
+
137
+ async def retrieve_post_preview_mapping(self, *, post_ids: set[str]) -> dict[str, PostPreview]:
138
+ return await self.repository.select_post_preview_mapping(post_ids=post_ids)
139
+
140
+ async def retrieve_post_by_id(self, *, post_id: str, viewer_id: str | None) -> Post | None:
141
+ return await self.repository.select_post_by_id(post_id=post_id, viewer_id=viewer_id)
142
+
143
+ async def retrieve_post_with_metadata_by_id(self, *, post_id: str) -> PostWithMetadata:
144
+ return await self.repository.select_post_with_metadata_by_id(post_id=post_id)
145
+
146
+ async def set_post_status_by_id(self, *, post_id: str, status: PostStatus) -> None:
147
+ updated = await self.repository.update_post_status_by_id(
148
+ post_id=post_id,
149
+ status=status,
150
+ )
151
+ if not updated:
152
+ raise PostNotFoundException()
153
+
154
+ async def delete_post_by_id(self, *, post_id: str) -> None:
155
+ deleted = await self.repository.delete_post_by_id(post_id=post_id)
156
+ if not deleted:
157
+ raise PostNotFoundException()
158
+
159
+ async def retrieve_posts(self, *, viewer_id: str | None, cursor: datetime, limit: int) -> list[Post]:
160
+ return await self.repository.select_posts(viewer_id=viewer_id, cursor=cursor, limit=limit)
161
+
162
+ async def retrieve_posts_by_user_id(
163
+ self,
164
+ *,
165
+ user_id: str,
166
+ cursor: datetime,
167
+ limit: int,
168
+ viewer_id: str | None,
169
+ ) -> list[Post]:
170
+ return await self.repository.select_posts_by_user_id(
171
+ user_id=user_id,
172
+ cursor=cursor,
173
+ limit=limit,
174
+ viewer_id=viewer_id,
175
+ )
176
+
177
+ async def retrieve_posts_by_hashtag(
178
+ self,
179
+ *,
180
+ hashtag: str,
181
+ cursor: datetime,
182
+ limit: int,
183
+ viewer_id: str | None,
184
+ ) -> list[Post]:
185
+ return await self.repository.select_posts_by_hashtag(
186
+ hashtag=hashtag,
187
+ cursor=cursor,
188
+ limit=limit,
189
+ viewer_id=viewer_id,
190
+ )
191
+
192
+ async def retrieve_posts_unfiltered(self, *, cursor: datetime, limit: int) -> list[PostWithMetadata]:
193
+ return await self.repository.select_posts_unfiltered(
194
+ cursor=cursor,
195
+ limit=limit,
196
+ )
197
+
198
+ async def retrieve_post_stats_mapping(self, *, post_ids: set[str]) -> defaultdict[str, PostStats]:
199
+ post_stats_mapping = await self.repository.select_post_stats_mapping(post_ids=post_ids)
200
+ return defaultdict(lambda: PostStats.DEFAULT, post_stats_mapping)
201
+
202
+ async def retrieve_hashtags_mapping(self, *, post_ids: set[str]) -> defaultdict[str, list[str]]:
203
+ hashtags_mapping = await self.repository.select_hashtags_mapping(post_ids=post_ids)
204
+ return defaultdict(list, hashtags_mapping)
@@ -0,0 +1,74 @@
1
+ # User Domain
2
+
3
+ ## Scope
4
+
5
+ Owns user identity, authorization role, user-to-user relationships, preferences, and login telemetry. Authentication is handled externally.
6
+
7
+ ## Owns
8
+
9
+ ### Identity
10
+
11
+ - User identity is represented by a Firebase UID
12
+ - Username is user-controlled and unique
13
+ - Creation time is immutable
14
+
15
+ ### Authorization
16
+
17
+ - Each user has exactly one role
18
+ - Roles define administrative authority only
19
+ - Role hierarchy is enforced by application-level authorization logic
20
+
21
+ ### User-to-User Relationships
22
+
23
+ - Block relationships (user-controlled, personal preference)
24
+ - Directed relationships owned by the blocking user
25
+
26
+ ### User Preferences
27
+
28
+ - User-configurable settings owned by the user
29
+ - Preference validation is enforced by domain logic
30
+
31
+ ## Does Not Own
32
+
33
+ - Authentication or credentials
34
+ - Account restriction state (muted/banned) - owned by moderation domain
35
+ - Moderation policy, enforcement actions, or appeals
36
+ - Notification delivery mechanisms
37
+
38
+ ## Invariants
39
+
40
+ - user_id is immutable
41
+ - username is globally unique
42
+ - Each user has exactly one role
43
+ - Role hierarchy is enforced in authorization logic
44
+
45
+ ## Persistence Model
46
+
47
+ ### Tables
48
+
49
+ #### users
50
+
51
+ - Core user record identified by Firebase UID
52
+ - Stores identity, creation metadata, and authorization role
53
+
54
+ #### user_blocks
55
+
56
+ - Tracks user-to-user block relationships
57
+ - Directed relationship
58
+
59
+ #### user_preferences
60
+
61
+ - User-configurable settings (theme, language)
62
+ - One row per user, created on user registration
63
+
64
+ #### user_login_events
65
+
66
+ - Append-only record of user login telemetry (id, user_id, created_at, request_context)
67
+ - user_id is the authenticated Firebase user (viewer_id from request_context), not from the event payload
68
+ - request_context (JSONB) holds fields from the request: viewer_id, provider, country_code, client_type, browser_name, os_name, referrer
69
+ - Used for audit, security, and analytics purposes
70
+
71
+ #### user_deletions
72
+
73
+ - Tracks scheduled account deletions
74
+ - Stores scheduled deletion timestamp
@@ -0,0 +1,39 @@
1
+ from core_framework.domains.user.constants import DEFAULT_USER_ID, REDACTED_AUTHOR_ID, SYSTEM_USER_ID
2
+ from core_framework.domains.user.enums import ProfileVisibility, Theme, UserRole
3
+ from core_framework.domains.user.exceptions import (
4
+ BaseUserException,
5
+ SelfBlockException,
6
+ UserCreationException,
7
+ UserIdConflictException,
8
+ UsernameConflictException,
9
+ )
10
+ from core_framework.domains.user.models import (
11
+ BlockedUser,
12
+ CreatedUser,
13
+ Preferences,
14
+ Profile,
15
+ UserChangeHistory,
16
+ UserIdentity,
17
+ UserWithProfile,
18
+ )
19
+
20
+ __all__ = [
21
+ "DEFAULT_USER_ID",
22
+ "REDACTED_AUTHOR_ID",
23
+ "SYSTEM_USER_ID",
24
+ "SelfBlockException",
25
+ "BaseUserException",
26
+ "UserIdConflictException",
27
+ "UsernameConflictException",
28
+ "UserCreationException",
29
+ "CreatedUser",
30
+ "BlockedUser",
31
+ "ProfileVisibility",
32
+ "Theme",
33
+ "UserRole",
34
+ "Preferences",
35
+ "Profile",
36
+ "UserIdentity",
37
+ "UserChangeHistory",
38
+ "UserWithProfile",
39
+ ]
@@ -0,0 +1,8 @@
1
+ from typing import Final
2
+
3
+ DEFAULT_USER_ID: Final[str] = "DEFAULT_USER_ID"
4
+ DEFAULT_USERNAME: Final[str] = "UserNotFound"
5
+ SYSTEM_USER_ID: Final[str] = "SYSTEM"
6
+ SYSTEM_USERNAME: Final[str] = "System"
7
+ REDACTED_AUTHOR_ID: Final[str] = "DELETED"
8
+ REDACTED_AUTHOR_USERNAME: Final[str] = "DeletedUser"
@@ -0,0 +1,29 @@
1
+ from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
2
+ from core_framework.domains.user.repository import UserRepository
3
+ from core_framework.domains.user.service import UserService
4
+
5
+
6
+ def build_user_repository(runtime: CoreRuntime) -> UserRepository:
7
+ return UserRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
8
+
9
+
10
+ def build_user_service(runtime: CoreRuntime) -> UserService:
11
+ return UserService(build_user_repository(runtime))
12
+
13
+
14
+ def configure_user_dependencies(runtime: CoreRuntime) -> None:
15
+ global user_repository, user_service
16
+ user_repository = build_user_repository(runtime)
17
+ user_service = build_user_service(runtime)
18
+
19
+
20
+ user_repository = unconfigured_dependency("UserRepository")
21
+ user_service = unconfigured_dependency("UserService")
22
+
23
+ __all__ = [
24
+ "build_user_repository",
25
+ "build_user_service",
26
+ "configure_user_dependencies",
27
+ "user_repository",
28
+ "user_service",
29
+ ]
@@ -0,0 +1,19 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class UserRole(StrEnum):
5
+ ADMIN = "admin"
6
+ MODERATOR = "moderator"
7
+ MEMBER = "member"
8
+
9
+
10
+ class ProfileVisibility(StrEnum):
11
+ PUBLIC = "public"
12
+ PRIVATE = "private"
13
+ MEMBERS_ONLY = "members_only"
14
+
15
+
16
+ class Theme(StrEnum):
17
+ DARK = "dark"
18
+ LIGHT = "light"
19
+ SYSTEM = "system"
@@ -0,0 +1,31 @@
1
+ class BaseUserException(Exception):
2
+ message: str
3
+
4
+ def __init__(self, message: str):
5
+ self.message = message
6
+ super().__init__(self.message)
7
+
8
+
9
+ class UserIdConflictException(BaseUserException):
10
+ def __init__(self, user_id: str):
11
+ super().__init__(f"User ID {user_id} already exists")
12
+
13
+
14
+ class UsernameConflictException(BaseUserException):
15
+ def __init__(self, username: str):
16
+ super().__init__(f"Username {username} already exists")
17
+
18
+
19
+ class UserCreationException(BaseUserException):
20
+ def __init__(self, user_id: str):
21
+ super().__init__(f"Failed to create user {user_id}")
22
+
23
+
24
+ class SelfBlockException(BaseUserException):
25
+ def __init__(self):
26
+ super().__init__("You cannot block yourself")
27
+
28
+
29
+ class DomainUserNotFoundException(BaseUserException):
30
+ def __init__(self):
31
+ super().__init__("User not found")
@@ -0,0 +1,124 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import ClassVar
4
+
5
+ from core_framework.domains.user.constants import (
6
+ DEFAULT_USER_ID,
7
+ DEFAULT_USERNAME,
8
+ REDACTED_AUTHOR_ID,
9
+ REDACTED_AUTHOR_USERNAME,
10
+ SYSTEM_USER_ID,
11
+ SYSTEM_USERNAME,
12
+ )
13
+ from core_framework.domains.user.enums import ProfileVisibility, Theme, UserRole
14
+
15
+
16
+ @dataclass(frozen=True, slots=True, kw_only=True)
17
+ class CreatedUser:
18
+ user_id: str
19
+ username: str
20
+
21
+
22
+ @dataclass(frozen=True, slots=True, kw_only=True)
23
+ class BlockedUser:
24
+ user_id: str
25
+ username: str
26
+ created_at: datetime
27
+
28
+
29
+ @dataclass(frozen=True, slots=True, kw_only=True)
30
+ class Preferences:
31
+ theme: Theme
32
+ language: str
33
+
34
+ DEFAULT: ClassVar[Preferences]
35
+
36
+
37
+ Preferences.DEFAULT = Preferences(
38
+ theme=Theme.LIGHT,
39
+ language="en",
40
+ )
41
+
42
+
43
+ @dataclass(frozen=True, slots=True, kw_only=True)
44
+ class Profile:
45
+ display_name: str | None
46
+ avatar_id: str | None
47
+ banner_id: str | None
48
+ bio: str | None
49
+ status: str | None
50
+ social_links: dict[str, str]
51
+ profile_visibility: ProfileVisibility
52
+
53
+ DEFAULT: ClassVar[Profile]
54
+
55
+
56
+ Profile.DEFAULT = Profile(
57
+ display_name=None,
58
+ avatar_id=None,
59
+ banner_id=None,
60
+ bio=None,
61
+ status=None,
62
+ social_links={},
63
+ profile_visibility=ProfileVisibility.PUBLIC,
64
+ )
65
+
66
+
67
+ @dataclass(frozen=True, slots=True, kw_only=True)
68
+ class UserIdentity:
69
+ user_id: str
70
+ username: str
71
+ display_name: str | None
72
+ role: UserRole
73
+ avatar_id: str | None
74
+ created_at: datetime
75
+
76
+ DEFAULT: ClassVar[UserIdentity]
77
+ SYSTEM: ClassVar[UserIdentity]
78
+ REDACTED_AUTHOR: ClassVar[UserIdentity]
79
+
80
+
81
+ UserIdentity.DEFAULT = UserIdentity(
82
+ user_id=DEFAULT_USER_ID,
83
+ username=DEFAULT_USERNAME,
84
+ display_name=None,
85
+ role=UserRole.MEMBER,
86
+ avatar_id=None,
87
+ created_at=datetime(1970, 1, 1),
88
+ )
89
+
90
+ UserIdentity.SYSTEM = UserIdentity(
91
+ user_id=SYSTEM_USER_ID,
92
+ username=SYSTEM_USERNAME,
93
+ display_name=None,
94
+ role=UserRole.MEMBER,
95
+ avatar_id=None,
96
+ created_at=datetime(1970, 1, 1),
97
+ )
98
+
99
+ UserIdentity.REDACTED_AUTHOR = UserIdentity(
100
+ user_id=REDACTED_AUTHOR_ID,
101
+ username=REDACTED_AUTHOR_USERNAME,
102
+ display_name=None,
103
+ role=UserRole.MEMBER,
104
+ avatar_id=None,
105
+ created_at=datetime(1970, 1, 1),
106
+ )
107
+
108
+
109
+ @dataclass(frozen=True, slots=True, kw_only=True)
110
+ class UserWithProfile:
111
+ identity: UserIdentity
112
+ profile: Profile
113
+ deletion_scheduled_for: datetime | None
114
+
115
+
116
+ @dataclass(frozen=True, slots=True, kw_only=True)
117
+ class UserChangeHistory:
118
+ user_id: str
119
+ actor_id: str | None
120
+ entity_type: str
121
+ field: str
122
+ old_value: str | None
123
+ new_value: str | None
124
+ created_at: datetime