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.
- core_framework/__init__.py +0 -0
- core_framework/alembic/comment/alembic/README +1 -0
- core_framework/alembic/comment/alembic/env.py +72 -0
- core_framework/alembic/comment/alembic/script.py.mako +28 -0
- core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
- core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
- core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
- core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
- core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
- core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
- core_framework/alembic/comment/alembic.ini +52 -0
- core_framework/alembic/extension/alembic/README +1 -0
- core_framework/alembic/extension/alembic/env.py +98 -0
- core_framework/alembic/extension/alembic/script.py.mako +28 -0
- core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
- core_framework/alembic/extension/alembic.ini +147 -0
- core_framework/alembic/moderation/alembic/README +1 -0
- core_framework/alembic/moderation/alembic/env.py +98 -0
- core_framework/alembic/moderation/alembic/script.py.mako +28 -0
- core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
- core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
- core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
- core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
- core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
- core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
- core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
- core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
- core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
- core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
- core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
- core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
- core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
- core_framework/alembic/moderation/alembic.ini +147 -0
- core_framework/alembic/post/alembic/README +1 -0
- core_framework/alembic/post/alembic/env.py +97 -0
- core_framework/alembic/post/alembic/script.py.mako +28 -0
- core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
- core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
- core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
- core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
- core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
- core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
- core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
- core_framework/alembic/post/alembic.ini +149 -0
- core_framework/alembic/user/alembic/README +1 -0
- core_framework/alembic/user/alembic/env.py +98 -0
- core_framework/alembic/user/alembic/script.py.mako +28 -0
- core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
- core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
- core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
- core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
- core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
- core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
- core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
- core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
- core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
- core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
- core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
- core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
- core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
- core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
- core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
- core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
- core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
- core_framework/alembic/user/alembic.ini +147 -0
- core_framework/api/__init__.py +0 -0
- core_framework/api/admin/__init__.py +0 -0
- core_framework/api/admin/comments/router.py +69 -0
- core_framework/api/admin/comments/schemas.py +53 -0
- core_framework/api/admin/moderation/__init__.py +0 -0
- core_framework/api/admin/moderation/router.py +205 -0
- core_framework/api/admin/moderation/schemas.py +110 -0
- core_framework/api/admin/posts/router.py +62 -0
- core_framework/api/admin/posts/schemas.py +29 -0
- core_framework/api/admin/router.py +17 -0
- core_framework/api/admin/users/__init__.py +0 -0
- core_framework/api/admin/users/router.py +181 -0
- core_framework/api/admin/users/schemas.py +137 -0
- core_framework/api/auth/__init__.py +0 -0
- core_framework/api/auth/router.py +21 -0
- core_framework/api/auth/schemas.py +28 -0
- core_framework/api/comments/authenticated/router.py +126 -0
- core_framework/api/comments/authenticated/schemas.py +27 -0
- core_framework/api/comments/public/router.py +103 -0
- core_framework/api/comments/public/schemas.py +36 -0
- core_framework/api/comments/router.py +9 -0
- core_framework/api/comments/schemas.py +17 -0
- core_framework/api/dependencies.py +168 -0
- core_framework/api/events/router.py +39 -0
- core_framework/api/events/schemas.py +20 -0
- core_framework/api/posts/authenticated/router.py +83 -0
- core_framework/api/posts/authenticated/schemas.py +37 -0
- core_framework/api/posts/public/router.py +100 -0
- core_framework/api/posts/public/schemas.py +39 -0
- core_framework/api/posts/router.py +9 -0
- core_framework/api/posts/schemas.py +39 -0
- core_framework/api/router.py +19 -0
- core_framework/api/schemas.py +9 -0
- core_framework/api/system/__init__.py +0 -0
- core_framework/api/system/router.py +108 -0
- core_framework/api/users/__init__.py +0 -0
- core_framework/api/users/authenticated/__init__.py +0 -0
- core_framework/api/users/authenticated/router.py +244 -0
- core_framework/api/users/authenticated/schemas.py +81 -0
- core_framework/api/users/public/__init__.py +0 -0
- core_framework/api/users/public/router.py +25 -0
- core_framework/api/users/public/schemas.py +7 -0
- core_framework/api/users/router.py +9 -0
- core_framework/api/users/shared/schemas.py +174 -0
- core_framework/application/__init__.py +0 -0
- core_framework/application/auth/__init__.py +0 -0
- core_framework/application/auth/access_service.py +26 -0
- core_framework/application/auth/auth_service.py +10 -0
- core_framework/application/auth/models.py +10 -0
- core_framework/application/bootstrap.py +19 -0
- core_framework/application/comments/admin_service.py +236 -0
- core_framework/application/comments/aggregation_service.py +28 -0
- core_framework/application/comments/authenticated_service.py +89 -0
- core_framework/application/comments/public_service.py +218 -0
- core_framework/application/events/README.md +26 -0
- core_framework/application/events/event_service.py +51 -0
- core_framework/application/events/event_token.py +46 -0
- core_framework/application/events/models.py +9 -0
- core_framework/application/moderation/__init__.py +0 -0
- core_framework/application/moderation/appeal_service.py +98 -0
- core_framework/application/moderation/moderator_service.py +46 -0
- core_framework/application/moderation/report_service.py +180 -0
- core_framework/application/moderation/scheduled_service.py +5 -0
- core_framework/application/moderation/user_service.py +180 -0
- core_framework/application/posts/admin_service.py +104 -0
- core_framework/application/posts/aggregation_service.py +28 -0
- core_framework/application/posts/authenticated_service.py +72 -0
- core_framework/application/posts/public_service.py +197 -0
- core_framework/application/shared/__init__.py +0 -0
- core_framework/application/shared/enums.py +16 -0
- core_framework/application/shared/exceptions.py +16 -0
- core_framework/application/shared/user_agent.py +24 -0
- core_framework/application/users/__init__.py +0 -0
- core_framework/application/users/admin_service.py +298 -0
- core_framework/application/users/authenticated_service.py +179 -0
- core_framework/application/users/public_service.py +7 -0
- core_framework/application/users/scheduled_service.py +5 -0
- core_framework/bundled_alembic.py +57 -0
- core_framework/core/__init__.py +37 -0
- core_framework/core/cache.py +234 -0
- core_framework/core/context.py +14 -0
- core_framework/core/database.py +111 -0
- core_framework/core/exception_handlers/__init__.py +3 -0
- core_framework/core/exception_handlers/comment.py +99 -0
- core_framework/core/exception_handlers/common.py +5 -0
- core_framework/core/exception_handlers/moderation.py +104 -0
- core_framework/core/exception_handlers/post.py +54 -0
- core_framework/core/exception_handlers/setup.py +80 -0
- core_framework/core/exception_handlers/user.py +72 -0
- core_framework/core/http_client.py +64 -0
- core_framework/core/logging.py +99 -0
- core_framework/core/middleware.py +64 -0
- core_framework/core/observability.py +36 -0
- core_framework/core/pagination.py +203 -0
- core_framework/core/redis.py +135 -0
- core_framework/core/runtime.py +66 -0
- core_framework/core/settings.py +189 -0
- core_framework/domains/__init__.py +0 -0
- core_framework/domains/comment/README.md +243 -0
- core_framework/domains/comment/__init__.py +25 -0
- core_framework/domains/comment/constants.py +3 -0
- core_framework/domains/comment/dependencies.py +29 -0
- core_framework/domains/comment/enums.py +11 -0
- core_framework/domains/comment/exceptions.py +31 -0
- core_framework/domains/comment/models.py +54 -0
- core_framework/domains/comment/repository.py +947 -0
- core_framework/domains/comment/service.py +259 -0
- core_framework/domains/moderation/README.md +138 -0
- core_framework/domains/moderation/__init__.py +47 -0
- core_framework/domains/moderation/dependencies.py +29 -0
- core_framework/domains/moderation/enums.py +62 -0
- core_framework/domains/moderation/exceptions.py +31 -0
- core_framework/domains/moderation/models.py +94 -0
- core_framework/domains/moderation/repository.py +828 -0
- core_framework/domains/moderation/service.py +334 -0
- core_framework/domains/post/README.md +182 -0
- core_framework/domains/post/__init__.py +22 -0
- core_framework/domains/post/constants.py +3 -0
- core_framework/domains/post/dependencies.py +29 -0
- core_framework/domains/post/enums.py +18 -0
- core_framework/domains/post/exceptions.py +21 -0
- core_framework/domains/post/models.py +53 -0
- core_framework/domains/post/repository.py +791 -0
- core_framework/domains/post/service.py +204 -0
- core_framework/domains/user/README.md +74 -0
- core_framework/domains/user/__init__.py +39 -0
- core_framework/domains/user/constants.py +8 -0
- core_framework/domains/user/dependencies.py +29 -0
- core_framework/domains/user/enums.py +19 -0
- core_framework/domains/user/exceptions.py +31 -0
- core_framework/domains/user/models.py +124 -0
- core_framework/domains/user/repository.py +612 -0
- core_framework/domains/user/service.py +257 -0
- core_framework/domains/user/utils.py +182 -0
- core_framework/main.py +104 -0
- core_framework/worker/__init__.py +0 -0
- core_framework/worker/main.py +56 -0
- core_framework/worker/schedules/__init__.py +35 -0
- core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
- core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
- core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
- core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
- core_framework/worker/tasks/__init__.py +11 -0
- core_framework/worker/tasks/process_account_deletion.py +13 -0
- core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
- core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
- core_framework/worker/tasks/process_mute_lift.py +13 -0
- core_framework-0.3.0.dist-info/METADATA +22 -0
- core_framework-0.3.0.dist-info/RECORD +222 -0
- 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
|