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,244 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, Query, Request, status
|
|
4
|
+
|
|
5
|
+
from core_framework.api.dependencies import RequiredFirebaseUser, RequiredUserID, check_not_banned
|
|
6
|
+
from core_framework.api.users.authenticated.schemas import (
|
|
7
|
+
AccountResponse,
|
|
8
|
+
AccountUpdateRequest,
|
|
9
|
+
AppealRequest,
|
|
10
|
+
BlockedUserResponse,
|
|
11
|
+
MyAppealsResponse,
|
|
12
|
+
PreferencesResponse,
|
|
13
|
+
PreferencesUpdateRequest,
|
|
14
|
+
ProfileResponse,
|
|
15
|
+
ProfileUpdateRequest,
|
|
16
|
+
UserReportRequest,
|
|
17
|
+
)
|
|
18
|
+
from core_framework.api.users.shared.schemas import validate_user_id
|
|
19
|
+
from core_framework.application.users.authenticated_service import (
|
|
20
|
+
add_appeal,
|
|
21
|
+
add_user_report,
|
|
22
|
+
block_user,
|
|
23
|
+
cancel_account_deletion,
|
|
24
|
+
change_my_account,
|
|
25
|
+
change_my_preferences,
|
|
26
|
+
change_my_profile,
|
|
27
|
+
delete_my_current_appeal,
|
|
28
|
+
remove_user_report,
|
|
29
|
+
retrieve_my_account,
|
|
30
|
+
retrieve_my_appeals,
|
|
31
|
+
retrieve_my_blocked_users,
|
|
32
|
+
retrieve_my_preferences,
|
|
33
|
+
retrieve_my_profile,
|
|
34
|
+
schedule_account_deletion,
|
|
35
|
+
unblock_user,
|
|
36
|
+
)
|
|
37
|
+
from core_framework.core.pagination import (
|
|
38
|
+
PaginationCursorType,
|
|
39
|
+
PaginationResponse,
|
|
40
|
+
TokenCursorQueryParams,
|
|
41
|
+
paginate_cursor,
|
|
42
|
+
)
|
|
43
|
+
from core_framework.domains.moderation import AppealDecision
|
|
44
|
+
|
|
45
|
+
router = APIRouter(
|
|
46
|
+
prefix="/users",
|
|
47
|
+
tags=["users - authenticated"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get(
|
|
52
|
+
"/me/blocks",
|
|
53
|
+
response_model=PaginationResponse[BlockedUserResponse],
|
|
54
|
+
dependencies=[Depends(check_not_banned)],
|
|
55
|
+
)
|
|
56
|
+
async def get_my_blocked_users(
|
|
57
|
+
request: Request,
|
|
58
|
+
user_id: RequiredUserID,
|
|
59
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
60
|
+
) -> Any:
|
|
61
|
+
blocked_users = await retrieve_my_blocked_users(
|
|
62
|
+
user_id=user_id.root, created_at=pagination_params.cursor, limit=pagination_params.limit
|
|
63
|
+
)
|
|
64
|
+
return paginate_cursor(
|
|
65
|
+
request,
|
|
66
|
+
blocked_users,
|
|
67
|
+
pagination_params.limit,
|
|
68
|
+
PaginationCursorType.CREATED_AT,
|
|
69
|
+
retrieved=pagination_params.retrieved,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get(
|
|
74
|
+
"/me/preferences",
|
|
75
|
+
response_model=PreferencesResponse,
|
|
76
|
+
dependencies=[Depends(check_not_banned)],
|
|
77
|
+
)
|
|
78
|
+
async def get_my_preferences(user_id: RequiredUserID) -> Any:
|
|
79
|
+
return await retrieve_my_preferences(user_id=user_id.root)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.patch(
|
|
83
|
+
"/me/preferences",
|
|
84
|
+
response_model=PreferencesResponse,
|
|
85
|
+
dependencies=[Depends(check_not_banned)],
|
|
86
|
+
)
|
|
87
|
+
async def patch_my_preferences(user_id: RequiredUserID, update_request: PreferencesUpdateRequest) -> Any:
|
|
88
|
+
validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
|
|
89
|
+
return await change_my_preferences(user_id=user_id.root, validated_update_request=validated_update_request)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@router.get(
|
|
93
|
+
"/me/profile",
|
|
94
|
+
response_model=ProfileResponse,
|
|
95
|
+
dependencies=[Depends(check_not_banned)],
|
|
96
|
+
)
|
|
97
|
+
async def get_my_profile(user_id: RequiredUserID) -> Any:
|
|
98
|
+
return await retrieve_my_profile(user_id=user_id.root)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.patch(
|
|
102
|
+
"/me/profile",
|
|
103
|
+
response_model=ProfileResponse,
|
|
104
|
+
dependencies=[Depends(check_not_banned)],
|
|
105
|
+
)
|
|
106
|
+
async def patch_my_profile(user_id: RequiredUserID, update_request: ProfileUpdateRequest) -> Any:
|
|
107
|
+
validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
|
|
108
|
+
return await change_my_profile(user_id=user_id.root, validated_update_request=validated_update_request)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get(
|
|
112
|
+
"/me/account",
|
|
113
|
+
response_model=AccountResponse,
|
|
114
|
+
dependencies=[Depends(check_not_banned)],
|
|
115
|
+
)
|
|
116
|
+
async def get_my_account(firebase_user: RequiredFirebaseUser) -> Any:
|
|
117
|
+
return await retrieve_my_account(
|
|
118
|
+
user_id=firebase_user.user_id.root,
|
|
119
|
+
email=firebase_user.email,
|
|
120
|
+
email_verified=firebase_user.email_verified,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.patch(
|
|
125
|
+
"/me/account",
|
|
126
|
+
response_model=AccountResponse,
|
|
127
|
+
dependencies=[Depends(check_not_banned)],
|
|
128
|
+
)
|
|
129
|
+
async def patch_my_account(
|
|
130
|
+
firebase_user: RequiredFirebaseUser,
|
|
131
|
+
update_request: AccountUpdateRequest,
|
|
132
|
+
) -> Any:
|
|
133
|
+
validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
|
|
134
|
+
await change_my_account(user_id=firebase_user.user_id.root, validated_update_request=validated_update_request)
|
|
135
|
+
return await retrieve_my_account(
|
|
136
|
+
user_id=firebase_user.user_id.root,
|
|
137
|
+
email=firebase_user.email,
|
|
138
|
+
email_verified=firebase_user.email_verified,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.put(
|
|
143
|
+
"/me/account/deletion",
|
|
144
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
145
|
+
dependencies=[Depends(check_not_banned)],
|
|
146
|
+
)
|
|
147
|
+
async def put_my_account_deletion(user_id: RequiredUserID) -> None:
|
|
148
|
+
await schedule_account_deletion(user_id=user_id.root)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.delete(
|
|
152
|
+
"/me/account/deletion",
|
|
153
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
154
|
+
dependencies=[Depends(check_not_banned)],
|
|
155
|
+
)
|
|
156
|
+
async def delete_my_account_deletion(user_id: RequiredUserID) -> None:
|
|
157
|
+
await cancel_account_deletion(user_id=user_id.root)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@router.get("/me/appeals", response_model=PaginationResponse[MyAppealsResponse])
|
|
161
|
+
async def get_my_appeals(
|
|
162
|
+
request: Request,
|
|
163
|
+
user_id: RequiredUserID,
|
|
164
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
165
|
+
status: Annotated[AppealDecision | None, Query()] = None,
|
|
166
|
+
) -> Any:
|
|
167
|
+
appeals = await retrieve_my_appeals(
|
|
168
|
+
user_id=user_id.root,
|
|
169
|
+
status=status,
|
|
170
|
+
cursor=pagination_params.cursor,
|
|
171
|
+
limit=pagination_params.limit,
|
|
172
|
+
)
|
|
173
|
+
return paginate_cursor(
|
|
174
|
+
request,
|
|
175
|
+
appeals,
|
|
176
|
+
pagination_params.limit,
|
|
177
|
+
PaginationCursorType.UPDATED_AT,
|
|
178
|
+
retrieved=pagination_params.retrieved,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.post("/me/appeals", status_code=status.HTTP_204_NO_CONTENT)
|
|
183
|
+
async def post_my_appeals(user_id: RequiredUserID, appeal_request: AppealRequest) -> None:
|
|
184
|
+
await add_appeal(user_id=user_id.root, justification=appeal_request.justification)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@router.delete("/me/appeals", status_code=status.HTTP_204_NO_CONTENT)
|
|
188
|
+
async def delete_my_appeal(user_id: RequiredUserID) -> None:
|
|
189
|
+
await delete_my_current_appeal(user_id=user_id.root)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.put(
|
|
193
|
+
"/{user_id}/block",
|
|
194
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
195
|
+
dependencies=[Depends(check_not_banned)],
|
|
196
|
+
)
|
|
197
|
+
async def put_block_user(requester_id: RequiredUserID, user_id: str) -> None:
|
|
198
|
+
validated_target_user_id = validate_user_id(user_id=user_id)
|
|
199
|
+
await block_user(user_id=requester_id.root, target_user_id=validated_target_user_id.root)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@router.delete(
|
|
203
|
+
"/{user_id}/block",
|
|
204
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
205
|
+
dependencies=[Depends(check_not_banned)],
|
|
206
|
+
)
|
|
207
|
+
async def delete_block_user(requester_id: RequiredUserID, user_id: str) -> None:
|
|
208
|
+
validated_target_user_id = validate_user_id(user_id=user_id)
|
|
209
|
+
await unblock_user(user_id=requester_id.root, target_user_id=validated_target_user_id.root)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router.post(
|
|
213
|
+
"/{user_id}/report",
|
|
214
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
215
|
+
dependencies=[Depends(check_not_banned)],
|
|
216
|
+
)
|
|
217
|
+
async def post_user_report(
|
|
218
|
+
requester_id: RequiredUserID,
|
|
219
|
+
user_id: str,
|
|
220
|
+
report_request: UserReportRequest,
|
|
221
|
+
) -> None:
|
|
222
|
+
validated_target_user_id = validate_user_id(user_id=user_id)
|
|
223
|
+
await add_user_report(
|
|
224
|
+
reporter_id=requester_id.root,
|
|
225
|
+
target_id=validated_target_user_id.root,
|
|
226
|
+
category=report_request.category,
|
|
227
|
+
reason=report_request.reason,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@router.delete(
|
|
232
|
+
"/{user_id}/report",
|
|
233
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
234
|
+
dependencies=[Depends(check_not_banned)],
|
|
235
|
+
)
|
|
236
|
+
async def delete_user_report(
|
|
237
|
+
requester_id: RequiredUserID,
|
|
238
|
+
user_id: str,
|
|
239
|
+
) -> None:
|
|
240
|
+
validated_target_user_id = validate_user_id(user_id=user_id)
|
|
241
|
+
await remove_user_report(
|
|
242
|
+
reporter_id=requester_id.root,
|
|
243
|
+
target_id=validated_target_user_id.root,
|
|
244
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
5
|
+
from ulid import ULID
|
|
6
|
+
|
|
7
|
+
from core_framework.api.schemas import BasePatchRequest
|
|
8
|
+
from core_framework.api.users.shared.schemas import (
|
|
9
|
+
AvatarMixin,
|
|
10
|
+
BannerMixin,
|
|
11
|
+
Bio,
|
|
12
|
+
DisplayName,
|
|
13
|
+
UserID,
|
|
14
|
+
Username,
|
|
15
|
+
UserReference,
|
|
16
|
+
UserStatus,
|
|
17
|
+
)
|
|
18
|
+
from core_framework.domains.moderation import AppealDecision, ReportCategory
|
|
19
|
+
from core_framework.domains.user import ProfileVisibility, Theme
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BlockedUserResponse(BaseModel):
|
|
23
|
+
user_id: UserID
|
|
24
|
+
username: Username
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProfileResponse(AvatarMixin, BannerMixin):
|
|
28
|
+
display_name: DisplayName | None
|
|
29
|
+
bio: Bio | None
|
|
30
|
+
status: UserStatus | None
|
|
31
|
+
social_links: dict[str, str]
|
|
32
|
+
profile_visibility: ProfileVisibility
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AccountResponse(BaseModel):
|
|
36
|
+
username: Username
|
|
37
|
+
email: EmailStr
|
|
38
|
+
email_verified: bool
|
|
39
|
+
deletion_scheduled_for: datetime | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AccountUpdateRequest(BasePatchRequest):
|
|
43
|
+
username: Username | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ProfileUpdateRequest(BasePatchRequest):
|
|
47
|
+
display_name: DisplayName | None = None
|
|
48
|
+
avatar_id: ULID | None = None
|
|
49
|
+
banner_id: ULID | None = None
|
|
50
|
+
bio: Bio | None = None
|
|
51
|
+
status: UserStatus | None = None
|
|
52
|
+
social_links: dict[str, str] | None = None
|
|
53
|
+
profile_visibility: ProfileVisibility | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UserReportRequest(BaseModel):
|
|
57
|
+
category: ReportCategory
|
|
58
|
+
reason: Annotated[str, Field(default="", max_length=250)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AppealRequest(BaseModel):
|
|
62
|
+
justification: Annotated[str, Field(..., min_length=50, max_length=1000)]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MyAppealsResponse(BaseModel):
|
|
66
|
+
justification: str
|
|
67
|
+
reviewer: UserReference | None
|
|
68
|
+
decision_reason: str | None
|
|
69
|
+
status: AppealDecision
|
|
70
|
+
created_at: datetime
|
|
71
|
+
updated_at: datetime
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PreferencesResponse(BaseModel):
|
|
75
|
+
theme: Theme
|
|
76
|
+
language: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PreferencesUpdateRequest(BasePatchRequest):
|
|
80
|
+
theme: Theme | None = None
|
|
81
|
+
language: Annotated[str | None, Field(default=None, max_length=10)]
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Query
|
|
4
|
+
from fastapi.exceptions import RequestValidationError
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from core_framework.api.users.public.schemas import UsernameExistsResponse
|
|
8
|
+
from core_framework.api.users.shared.schemas import Username
|
|
9
|
+
from core_framework.application.users.public_service import is_username_available
|
|
10
|
+
|
|
11
|
+
router = APIRouter(
|
|
12
|
+
prefix="/users",
|
|
13
|
+
tags=["users - public"],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/usernames/exists", response_model=UsernameExistsResponse)
|
|
18
|
+
async def get_username_exists(username: Annotated[str, Query()]) -> Any:
|
|
19
|
+
try:
|
|
20
|
+
validated_username = Username(
|
|
21
|
+
root=username
|
|
22
|
+
) # Note: Workaround for Pydantic's lack of support for Root Model in Query parameters
|
|
23
|
+
except ValidationError as e:
|
|
24
|
+
raise RequestValidationError(e.errors()) from e
|
|
25
|
+
return {"status": "available" if await is_username_available(username=validated_username.root) else "taken"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core_framework.api.users.authenticated.router import router as authenticated_router
|
|
4
|
+
from core_framework.api.users.public.router import router as public_router
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
router.include_router(public_router)
|
|
9
|
+
router.include_router(authenticated_router)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi.exceptions import RequestValidationError
|
|
4
|
+
from pydantic import (
|
|
5
|
+
BaseModel,
|
|
6
|
+
Field,
|
|
7
|
+
HttpUrl,
|
|
8
|
+
RootModel,
|
|
9
|
+
TypeAdapter,
|
|
10
|
+
ValidationError,
|
|
11
|
+
computed_field,
|
|
12
|
+
field_validator,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_http_url_adapter = TypeAdapter(HttpUrl)
|
|
16
|
+
_avatar_default_url: str | None = None
|
|
17
|
+
_avatar_base_url: str | None = None
|
|
18
|
+
_avatar_extension: str | None = None
|
|
19
|
+
_banner_default_url: str | None = None
|
|
20
|
+
_banner_base_url: str | None = None
|
|
21
|
+
_banner_extension: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def configure_user_media_urls(
|
|
25
|
+
*,
|
|
26
|
+
avatar_default_url: str,
|
|
27
|
+
avatar_base_url: str,
|
|
28
|
+
avatar_extension: str,
|
|
29
|
+
banner_default_url: str,
|
|
30
|
+
banner_base_url: str,
|
|
31
|
+
banner_extension: str,
|
|
32
|
+
) -> None:
|
|
33
|
+
global _avatar_default_url, _avatar_base_url, _avatar_extension
|
|
34
|
+
global _banner_default_url, _banner_base_url, _banner_extension
|
|
35
|
+
_avatar_default_url = avatar_default_url
|
|
36
|
+
_avatar_base_url = avatar_base_url.rstrip("/")
|
|
37
|
+
_avatar_extension = avatar_extension
|
|
38
|
+
_banner_default_url = banner_default_url
|
|
39
|
+
_banner_base_url = banner_base_url.rstrip("/")
|
|
40
|
+
_banner_extension = banner_extension
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_user_media_urls() -> dict[str, str]:
|
|
44
|
+
if (
|
|
45
|
+
_avatar_default_url is not None
|
|
46
|
+
and _avatar_base_url is not None
|
|
47
|
+
and _avatar_extension is not None
|
|
48
|
+
and _banner_default_url is not None
|
|
49
|
+
and _banner_base_url is not None
|
|
50
|
+
and _banner_extension is not None
|
|
51
|
+
):
|
|
52
|
+
return {
|
|
53
|
+
"avatar_default_url": _avatar_default_url,
|
|
54
|
+
"avatar_base_url": _avatar_base_url,
|
|
55
|
+
"avatar_extension": _avatar_extension,
|
|
56
|
+
"banner_default_url": _banner_default_url,
|
|
57
|
+
"banner_base_url": _banner_base_url,
|
|
58
|
+
"banner_extension": _banner_extension,
|
|
59
|
+
}
|
|
60
|
+
msg = "User media URLs are not configured. Call configure_user_media_urls() during application bootstrap."
|
|
61
|
+
raise RuntimeError(msg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class UserID(RootModel[str]):
|
|
65
|
+
root: Annotated[
|
|
66
|
+
str,
|
|
67
|
+
Field(
|
|
68
|
+
description="The ID of the user",
|
|
69
|
+
min_length=1,
|
|
70
|
+
max_length=128,
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Username(RootModel[str]):
|
|
76
|
+
root: Annotated[
|
|
77
|
+
str,
|
|
78
|
+
Field(
|
|
79
|
+
description="The username of the user",
|
|
80
|
+
min_length=1,
|
|
81
|
+
max_length=50,
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
@field_validator("root", mode="after")
|
|
86
|
+
@classmethod
|
|
87
|
+
def no_whitespace(cls, v: str) -> str:
|
|
88
|
+
if any(ch.isspace() for ch in v):
|
|
89
|
+
raise ValueError("Username must not contain whitespace")
|
|
90
|
+
return v
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Bio(RootModel[str]):
|
|
94
|
+
root: Annotated[
|
|
95
|
+
str,
|
|
96
|
+
Field(
|
|
97
|
+
description="User profile bio",
|
|
98
|
+
max_length=500,
|
|
99
|
+
),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class UserStatus(RootModel[str]):
|
|
104
|
+
root: Annotated[
|
|
105
|
+
str,
|
|
106
|
+
Field(
|
|
107
|
+
description="User status (e.g. away, online)",
|
|
108
|
+
max_length=150,
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class DisplayName(RootModel[str]):
|
|
114
|
+
root: Annotated[
|
|
115
|
+
str,
|
|
116
|
+
Field(
|
|
117
|
+
description="The display name of the user",
|
|
118
|
+
min_length=1,
|
|
119
|
+
max_length=100,
|
|
120
|
+
),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AvatarMixin(BaseModel):
|
|
125
|
+
avatar_id: Annotated[str | None, Field(exclude=True)]
|
|
126
|
+
|
|
127
|
+
@computed_field
|
|
128
|
+
@property
|
|
129
|
+
def avatar(self) -> HttpUrl:
|
|
130
|
+
url_string = _construct_avatar_url(avatar_id=self.avatar_id)
|
|
131
|
+
return _http_url_adapter.validate_python(url_string)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class BannerMixin(BaseModel):
|
|
135
|
+
banner_id: Annotated[str | None, Field(exclude=True)]
|
|
136
|
+
|
|
137
|
+
@computed_field
|
|
138
|
+
@property
|
|
139
|
+
def banner(self) -> HttpUrl:
|
|
140
|
+
url_string = _construct_banner_url(banner_id=self.banner_id)
|
|
141
|
+
return _http_url_adapter.validate_python(url_string)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UserReference(BaseModel):
|
|
145
|
+
user_id: UserID
|
|
146
|
+
username: Username
|
|
147
|
+
display_name: DisplayName | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def validate_user_id(*, user_id: str) -> UserID:
|
|
151
|
+
try:
|
|
152
|
+
return UserID(root=user_id) # Note: Workaround for Pydantic's lack of support for Root Model in Path parameters
|
|
153
|
+
except ValidationError as e:
|
|
154
|
+
raise RequestValidationError(e.errors()) from e
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def validate_optional_user_id(*, user_id: str | None) -> UserID | None:
|
|
158
|
+
if user_id is None:
|
|
159
|
+
return None
|
|
160
|
+
return validate_user_id(user_id=user_id)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _construct_avatar_url(*, avatar_id: str | None) -> str:
|
|
164
|
+
media_urls = _get_user_media_urls()
|
|
165
|
+
if avatar_id is None:
|
|
166
|
+
return media_urls["avatar_default_url"]
|
|
167
|
+
return f"{media_urls['avatar_base_url']}/{avatar_id}.{media_urls['avatar_extension']}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _construct_banner_url(*, banner_id: str | None) -> str:
|
|
171
|
+
media_urls = _get_user_media_urls()
|
|
172
|
+
if banner_id is None:
|
|
173
|
+
return media_urls["banner_default_url"]
|
|
174
|
+
return f"{media_urls['banner_base_url']}/{banner_id}.{media_urls['banner_extension']}"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
4
|
+
import core_framework.domains.user.dependencies as user_deps
|
|
5
|
+
from core_framework.application.auth.models import UserDetail
|
|
6
|
+
from core_framework.application.shared.enums import RedisKeys
|
|
7
|
+
from core_framework.core.cache import cache
|
|
8
|
+
from core_framework.domains.user import UserIdentity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@cache(
|
|
12
|
+
resource_name=RedisKeys.USER_DETAIL,
|
|
13
|
+
ttl=86_400, # 1 day
|
|
14
|
+
skip_if=lambda r: r.identity == UserIdentity.DEFAULT,
|
|
15
|
+
)
|
|
16
|
+
async def get_user_detail(*, user_id: str) -> UserDetail:
|
|
17
|
+
async with asyncio.TaskGroup() as tg:
|
|
18
|
+
identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids={user_id}))
|
|
19
|
+
moderation_task = tg.create_task(
|
|
20
|
+
moderation_deps.moderation_service.retrieve_user_moderation_mapping(user_ids={user_id})
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return UserDetail(
|
|
24
|
+
identity=identity_task.result()[user_id],
|
|
25
|
+
moderation=moderation_task.result()[user_id],
|
|
26
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import core_framework.core as core_fw
|
|
2
|
+
import core_framework.domains.user.dependencies as user_deps
|
|
3
|
+
from core_framework.application.shared.enums import RedisKeys
|
|
4
|
+
from core_framework.domains.user import CreatedUser
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def register_user(*, user_id: str) -> CreatedUser:
|
|
8
|
+
created_user = await user_deps.user_service.add_user(user_id=user_id)
|
|
9
|
+
await core_fw.redis_cache.sadd(RedisKeys.TAKEN_USERNAMES, created_user.username)
|
|
10
|
+
return created_user
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from core_framework.domains.moderation import UserModeration
|
|
4
|
+
from core_framework.domains.user import UserIdentity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
8
|
+
class UserDetail:
|
|
9
|
+
identity: UserIdentity
|
|
10
|
+
moderation: UserModeration
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from core_framework.core.cache import configure_cache
|
|
2
|
+
from core_framework.core.runtime import CoreRuntime
|
|
3
|
+
from core_framework.domains.comment.dependencies import configure_comment_dependencies
|
|
4
|
+
from core_framework.domains.moderation.dependencies import configure_moderation_dependencies
|
|
5
|
+
from core_framework.domains.post.dependencies import configure_post_dependencies
|
|
6
|
+
from core_framework.domains.user.dependencies import configure_user_dependencies
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_application_dependencies(runtime: CoreRuntime) -> None:
|
|
10
|
+
configure_cache(redis_cache=runtime.redis_cache)
|
|
11
|
+
configure_comment_dependencies(runtime)
|
|
12
|
+
configure_moderation_dependencies(runtime)
|
|
13
|
+
configure_post_dependencies(runtime)
|
|
14
|
+
configure_user_dependencies(runtime)
|
|
15
|
+
|
|
16
|
+
# alru_cache on user id set must not survive a rebound UserService instance
|
|
17
|
+
from core_framework.application.users.admin_service import retrieve_admin_user_ids
|
|
18
|
+
|
|
19
|
+
retrieve_admin_user_ids.cache_clear()
|